Compare commits

...

104 Commits

Author SHA1 Message Date
62154687c0 removed unused resources 2022-01-31 22:20:53 +01:00
Johannes Hochwart
375808a7d3 Netzwerkkomponente(#9) 2022-01-26 23:03:32 +01:00
Johannes Hochwart
6e3a626bad Anleitungsscreen(#18) 2022-01-26 15:02:03 +01:00
b0cab022ef fixed npe in CardUtil.getDefaultTrumpSuit 2022-01-17 18:19:28 +01:00
c47b796da1 added sync before determining trump 2022-01-17 15:41:19 +01:00
0eb93428c7 fixed server build script 2022-01-17 12:19:52 +01:00
24389b0962 improved error handling 2022-01-17 00:41:27 +01:00
22bb107cf2 fixed npe in Session 2022-01-17 00:38:49 +01:00
94df8c181f fixed bug in ChangePredictionOverlay 2022-01-16 23:47:36 +01:00
8f2a97bb82 improved error handling 2022-01-16 23:07:29 +01:00
35c0cfbc2a added "end card" 2022-01-15 00:47:17 +01:00
5281b592af added trick count label 2022-01-14 23:20:44 +01:00
167dd8344e bugfixes 2022-01-14 22:41:51 +01:00
43357f5c7b added support for changeling and improved support for cloud and juggler 2022-01-14 21:57:30 +01:00
20a9c1e046 improved instructions screen (#18) 2022-01-14 13:58:06 +01:00
Johannes Hochwart
4ffc37c75f Anleitungsscreen(#18) 2022-01-14 13:31:10 +01:00
659f85e5d3 multiple bugfixes 2022-01-14 09:15:10 +01:00
6b1bca63a7 Merge remote-tracking branch 'origin/main' into main 2022-01-14 09:01:03 +01:00
6ae84c7d07 font adjustments 2022-01-14 09:00:39 +01:00
Benedikt Riedl
b273c21b19 Sound Effects 2022-01-14 02:07:18 +01:00
Johannes Hochwart
fffe6968b1 Anleitungsscreen(#18) 2022-01-14 00:32:13 +01:00
Teubler
9a726335e1 #16 started useful error handling 2022-01-13 23:09:20 +01:00
Teubler
ed54407881 #16 added basic error screen 2022-01-13 22:30:09 +01:00
Teubler
b059c52476 #16 added internationalized messages 2022-01-13 22:25:39 +01:00
0e875937f3 rejoin support for libgdx client 2022-01-13 22:05:47 +01:00
448810eaba bugfixes 2022-01-13 22:04:54 +01:00
ec09d22ecd refactoring 2022-01-13 19:26:26 +01:00
9089403249 refactoring 2022-01-13 19:23:52 +01:00
00f0dd519e bugfixes 2022-01-13 16:30:09 +01:00
e702fa5a13 added name validation 2022-01-13 16:25:47 +01:00
48371e2167 fixed dimming behind overlays
added menu
2022-01-12 21:11:57 +01:00
78412052bf migration to asset manager 2022-01-12 16:01:49 +01:00
f1ee3f65ca improved menu accessibility 2022-01-12 14:44:04 +01:00
e6090880ec bugfixes 2022-01-12 12:51:22 +01:00
66172c20cb added debug websocket 2022-01-12 12:15:52 +01:00
3b31c752cb improved null safety 2022-01-12 11:35:02 +01:00
371d9d24a6 migrated to jackson 2022-01-12 10:28:31 +01:00
857a04cb9d bugfixes and improvements
* fixed inconsistent player order
* minor visual adjustments
* refactoring
2022-01-11 14:00:28 +01:00
8e61e6b2f8 support for juggling and multicolored cards 2022-01-10 22:00:37 +01:00
3800fb546f changes to sync 2022-01-10 20:22:07 +01:00
Benedikt Riedl
c15e5cff3a Kartenbilder jpg@250x400px 2022-01-10 16:10:59 +01:00
99e7e9c5ff internationalization of game screen 2022-01-10 15:12:42 +01:00
0a6f9eed60 game logic 2022-01-10 14:30:27 +01:00
5c0c9c280e added sync point after trump determination 2022-01-10 04:06:27 +01:00
b4ecfbb32e improved menus in libGDX client 2022-01-10 03:10:32 +01:00
c37ec96a57 improved libGDX client logging 2022-01-09 22:49:32 +01:00
c6dd76e653 added suit cards 2022-01-09 22:24:08 +01:00
0c646cea39 Updated Log4j2 dependency 2022-01-09 22:23:54 +01:00
cf6639c65f visual improvements 2021-12-14 19:28:02 +01:00
d6af607cf2 added sync at start of round and trick 2021-12-14 17:34:20 +01:00
abe305fc95 added TimeoutMessage 2021-12-14 16:26:02 +01:00
ba1fed5eb7 visual improvements 2021-12-14 14:00:59 +01:00
cd60bf615e improved CreateGameScreen 2021-12-14 12:55:18 +01:00
bd1ece0fee fixed game creation screen 6b4e688e #16 2021-12-14 11:16:26 +01:00
1427c05699 Update .gitlab-ci.yml file 2021-12-14 10:59:04 +01:00
b746d50973 Updated Dependencies 2021-12-14 09:03:10 +01:00
Teubler
6b4e688e20 #16 + updated CreateGameScreen
functionality of CreateGameScreen WIP
2021-12-09 21:36:40 +01:00
00af6e9794 fixed c3e10ae4 (#17) 2021-12-07 17:25:59 +01:00
f9d8721e37 reconnect in server and cli 2021-12-05 11:21:46 +01:00
Johannes Hochwart
c3e10ae4fa Spielscreen (#17) 2021-12-04 15:14:00 +01:00
08ae51a952 #17 2021-12-03 10:53:52 +01:00
b312e8faf4 Cleanup 2021-12-02 22:44:27 +01:00
da15bff83a Cleanup 2021-12-02 22:24:45 +01:00
Benedikt Riedl
47ed046ac1 Kartenbilder jpg@250x400px 2021-12-02 14:45:03 +01:00
Benedikt Riedl
31fdd86c8b Einzelgrafikelemente #7 2021-12-02 03:02:30 +01:00
2ab0a4d5bb shrank client size 2021-12-01 17:58:53 +01:00
71f66194e6 improved GameScreen 2021-12-01 14:28:03 +01:00
53992fc4cf bugfixes in server and cli-client 2021-11-30 11:23:41 +01:00
Alexander Hirmer
80864f0d44 #14 2021-11-29 15:56:32 +01:00
343147b339 CLI Client #15 2021-11-25 23:17:29 +01:00
66cec29844 CLI Client #15 2021-11-25 19:36:52 +01:00
4205475878 CLI Client #15 2021-11-25 19:33:31 +01:00
Johannes Hochwart
7b5de93741 Erste Grundlage für den Spielscreen (#17) 2021-11-25 19:28:14 +01:00
Johannes Hochwart
4cbafa116f Erste Grundlage für den Spielscreen (#17) 2021-11-25 19:19:55 +01:00
Alexander Hirmer
ae7e77ae7d Ticket #14 2021-11-23 16:00:14 +01:00
e7b99ee11f improved libGDX client performance
added automatic texture packing
2021-11-19 17:55:46 +01:00
6c5a499415 extracted dependencies info into Dependencies.kt 2021-11-19 10:42:31 +01:00
6a75def681 migration to gradle kotlin dsl 2021-11-19 09:59:40 +01:00
cc38e3f830 added distribution task to client buildscript 2021-11-17 12:31:35 +01:00
4e9cdb184c added parse method to messages 2021-11-15 18:29:25 +01:00
a0445890b9 Refactored LibGDX Client 2021-11-12 22:34:14 +01:00
Teubler
fb2afa2f09 Initial libGDX Code Commit #11 2021-11-12 18:34:00 +01:00
a1e05ddafc Sample LibGDX project setup 2021-11-12 18:16:28 +01:00
Benedikt Riedl
09ceee91d6 Einzelgrafikelemente #7 2021-11-12 10:25:55 +01:00
9341637198 Fixed Gradle Tests 2021-11-12 09:23:49 +01:00
692fb1cb2c Migration from Maven to Gradle 7.3 2021-11-12 09:06:59 +01:00
Johannes Hochwart
5dd7cc6eab Weitere Client- und ServerMessages für Verbindungsverlust und VoteCick 2021-11-11 13:06:08 +01:00
1bf5e127fe Reworked state machine 2021-11-11 09:48:59 +01:00
Johannes Hochwart
90625e536b Merge remote-tracking branch 'origin/main' 2021-11-10 20:27:40 +01:00
Johannes Hochwart
4b13b4ef16 Bugfixes 2021-11-10 20:27:19 +01:00
Benedikt Riedl
075ac3746e Upload New File 2021-11-09 12:38:29 +00:00
Benedikt Riedl
e0ce3cc80d Upload New File 2021-11-09 12:38:09 +00:00
Benedikt Riedl
f2462ff43e Upload New File 2021-11-09 12:37:33 +00:00
Benedikt Riedl
dfb21aaee5 Upload New File 2021-11-09 12:36:56 +00:00
Benedikt Riedl
8b3f93aca1 Menügrafiken 2021-11-09 12:36:06 +00:00
Benedikt Riedl
e8c911c38c Konzeptgrafiken 2021-11-09 12:35:45 +00:00
Johannes Hochwart
262ae4a2cc Client- und Server-Nachrichten verbessert(#9) 2021-11-05 12:43:58 +01:00
f4255d39ab Fehler in Spiellogik von Jongleur und Wolke behoben (#12) 2021-11-05 01:31:20 +01:00
Johannes Hochwart
6da10c6b43 Client- und Server-Nachrichten implementiert(#9) 2021-11-04 22:54:40 +01:00
66f6d10330 - Jubiläumsedition implementiert (#12)
- Tests verbessert
2021-11-04 20:18:26 +01:00
1775604e1d - refactored GameData
- removed reference to Context in State
2021-10-29 22:58:11 +02:00
Jonah Bauer
d04e894937 Update .gitlab-ci.yml file 2021-10-28 21:05:47 +00:00
d8453fa527 Issue #5: Grundspiel implementieren 2021-10-28 23:04:49 +02:00
a6f82361f7 project setup 2021-10-18 16:30:07 +02:00
362 changed files with 65000 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

46
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,46 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml
# This is the Gradle build system for JVM applications
# https://gradle.org/
# https://github.com/gradle/gradle
image: gradle:alpine
# Disable the Gradle daemon for Continuous Integration servers as correctness
# is usually a priority over speed in CI environments. Using a fresh
# runtime for each build is more reliable since the runtime is completely
# isolated from any previous builds.
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
build:
stage: build
script: gradle --build-cache assemble
cache:
key: "$CI_COMMIT_REF_NAME"
policy: push
paths:
- build
- .gradle
test:
stage: test
script: gradle check
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- build
- .gradle
artifacts:
when: always
reports:
junit:
- "**/build/test-results/test/**/TEST-*.xml"
- "**/build/reports/dependency-check-junit.xml"

74
build.gradle.kts Normal file
View File

@ -0,0 +1,74 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.owasp:dependency-check-gradle:6.5.0.1")
}
}
subprojects {
apply(plugin = "java-library")
apply(plugin = "org.owasp.dependencycheck")
group = "eu.jonahbauer"
version = "1.0-SNAPSHOT"
val implementation by configurations
val testImplementation by configurations
val compileOnly by configurations
val annotationProcessor by configurations
val testCompileOnly by configurations
val testAnnotationProcessor by configurations
val runtimeOnly by configurations
repositories {
mavenCentral()
}
dependencies {
implementation(Annotations.id)
implementation(Log4j2.api)
runtimeOnly(Log4j2.core)
runtimeOnly(Log4j2.slf4j)
testImplementation(JUnit.jupiter)
testImplementation(JUnit.jupiter_engine)
testImplementation(Mockito.inline)
testImplementation(Mockito.jupiter)
compileOnly(Lombok.id)
annotationProcessor(Lombok.id)
testCompileOnly(Lombok.id)
testAnnotationProcessor(Lombok.id)
}
tasks {
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
options.encoding = "UTF-8"
}
withType<Jar> {
onlyIf {
!project.the<SourceSetContainer>()["main"].allSource.isEmpty
}
}
named<Test>("test") {
useJUnitPlatform()
}
named("check") {
dependsOn("dependencyCheckAnalyze")
}
}
configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> {
format = org.owasp.dependencycheck.reporting.ReportGenerator.Format.JUNIT
}
}

12
buildSrc/build.gradle.kts Normal file
View File

@ -0,0 +1,12 @@
plugins {
kotlin("jvm") version "1.6.0"
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.badlogicgames.gdx:gdx-tools:1.10.0")
implementation(gradleApi())
}

View File

@ -0,0 +1,101 @@
@file:Suppress("MemberVisibilityCanBePrivate")
object Log4j2 {
const val version = "2.17.1"
const val group = "org.apache.logging.log4j"
const val api = "$group:log4j-api:$version"
const val core = "$group:log4j-core:$version"
const val slf4j = "$group:log4j-slf4j-impl:$version"
const val jul = "$group:log4j-jul:$version"
}
object Lombok {
const val version = "1.18.22"
const val group = "org.projectlombok"
const val id = "$group:lombok:$version"
}
object JUnit {
const val version = "5.8.1"
const val group = "org.junit.jupiter"
const val jupiter = "$group:junit-jupiter:$version"
const val jupiter_engine = "$group:junit-jupiter-engine:$version"
}
object Mockito {
const val version = "4.0.0"
const val group = "org.mockito"
const val inline = "$group:mockito-inline:$version"
const val jupiter = "$group:mockito-junit-jupiter:$version"
}
object Jackson {
const val version = "2.13.1"
const val databind = "com.fasterxml.jackson.core:jackson-databind:$version"
const val module_parameter_names = "com.fasterxml.jackson.module:jackson-module-parameter-names:$version"
}
object Annotations {
const val version = "22.0.0"
const val group = "org.jetbrains"
const val id = "$group:annotations:$version"
}
object LibGDX {
const val version = "1.10.0"
const val group = "com.badlogicgames.gdx"
const val api = "$group:gdx:$version"
const val backend_lwjgl3 = "$group:gdx-backend-lwjgl3:$version"
const val platform_desktop = "$group:gdx-platform:$version:natives-desktop"
const val tools = "$group:gdx-tools:$version"
}
object PicoCLI {
const val version = "4.6.2"
const val group = "info.picocli"
const val core = "$group:picocli:$version"
const val shell_jline3 = "$group:picocli-shell-jline3:$version"
const val codegen = "$group:picocli-codegen:$version"
}
object JLine {
const val version = "3.20.0"
const val group = "org.jline"
const val id = "$group:jline:$version"
}
object Jansi {
const val version = "2.4.0"
const val group = "org.fusesource.jansi"
const val id = "$group:jansi:$version"
}
object JavaWebSocket {
const val version = "1.5.2"
const val group = "org.java-websocket"
const val id = "$group:Java-WebSocket:$version"
}
object SpringBoot {
const val version = "2.6.2"
const val group = "org.springframework.boot"
const val plugin = group
const val starterWebsocket = "$group:spring-boot-starter-websocket"
const val starterTomcat = "$group:spring-boot-starter-tomcat"
const val starterLog4j2 = "$group:spring-boot-starter-log4j2"
}
object SpringDependencyManagement {
const val version = "1.0.11.RELEASE"
const val plugin = "io.spring.dependency-management"
}

View File

@ -0,0 +1,76 @@
import com.badlogic.gdx.tools.texturepacker.TexturePacker
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import java.io.File
import java.util.*
import java.util.regex.Pattern
import kotlin.collections.HashSet
abstract class TexturePackerTask : DefaultTask() {
@get:Incremental
@get:InputDirectory
abstract val input : DirectoryProperty
@get:OutputDirectory
abstract val resourceOutput : DirectoryProperty
@get:OutputDirectory
abstract val generatedSourceOutput : DirectoryProperty
@TaskAction
fun pack(changes: InputChanges) {
val dirs = HashSet<File>()
val inputDir = input.asFile.get()
if (changes.isIncremental) {
val root = inputDir.toPath()
for (change in changes.getFileChanges(input)) {
if (!change.file.isFile) continue
dirs.add(root.resolve(root.relativize(change.file.toPath()).subpath(0, 1)).toFile())
}
} else {
inputDir.listFiles()?.filter { it.isDirectory }?.forEach { dirs.add(it) }
}
val outputDir = resourceOutput.get().asFile
for (dir in dirs) {
TexturePacker.process(dir.path, outputDir.path, dir.name)
val atlas = File(outputDir, dir.name + ".atlas")
if (atlas.exists()) {
val name = dir.name[0].uppercaseChar() + dir.name.substring(1) + "Atlas"
val path = outputDir.toPath().relativize(atlas.toPath()).toString()
val builder = StringBuilder()
builder.append("""
package eu.jonahbauer.wizard.client.libgdx;
public class $name {
public static final String ${'$'}PATH = "$path";
""".trimIndent())
val content = atlas.readText(Charsets.UTF_8)
val matcher = Pattern.compile("(?m)^([^:]*?)\$\\n {2}").matcher(content)
val fields = HashSet<String>()
while (matcher.find()) {
val texture = matcher.group(1)
val field = texture.replace("-", "_").replace("/", "_").uppercase(Locale.ROOT)
fields.add(" public static final String $field = \"$texture\";\n")
}
fields.forEach(builder::append)
builder.append("}\n")
val out = generatedSourceOutput.file("eu/jonahbauer/wizard/client/libgdx/${name}.java").get().asFile
out.parentFile.mkdirs()
out.writeText(builder.toString(), Charsets.UTF_8)
}
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

11
settings.gradle.kts Normal file
View File

@ -0,0 +1,11 @@
/*
* This file was generated by the Gradle 'init' task.
*/
rootProject.name = "wizard"
include(":wizard-common")
include(":wizard-client:wizard-client-cli")
include(":wizard-client:wizard-client-libgdx:core")
include(":wizard-client:wizard-client-libgdx:desktop")
include(":wizard-server")
include(":wizard-core")

View File

View File

@ -0,0 +1,22 @@
plugins {
application
}
dependencies {
implementation(project(":wizard-common"))
implementation(JLine.id)
implementation(Jansi.id)
implementation(JavaWebSocket.id)
implementation("org.ajbrown:name-machine:1.0.0")
implementation(PicoCLI.core)
implementation(PicoCLI.shell_jline3) {
exclude(group = JLine.group) // prevent duplicates with org.jline:jline
}
annotationProcessor(PicoCLI.codegen)
}
application {
mainClass.set("eu.jonahbauer.wizard.client.cli.Client")
}

View File

@ -0,0 +1,273 @@
package eu.jonahbauer.wizard.client.cli;
import eu.jonahbauer.wizard.client.cli.state.ClientState;
import eu.jonahbauer.wizard.client.cli.state.Menu;
import eu.jonahbauer.wizard.client.cli.util.DelegateCompleter;
import eu.jonahbauer.wizard.client.cli.util.StateAwareFactory;
import eu.jonahbauer.wizard.common.machine.TimeoutContext;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.Getter;
import org.java_websocket.framing.CloseFrame;
import org.jline.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.shell.jline3.PicocliJLineCompleter;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static picocli.CommandLine.*;
@Command
public class Client extends TimeoutContext<ClientState, Client> implements Runnable {
@Option(names = "-v")
@Getter
private boolean verbose;
private LineReader reader;
@Getter
private ClientSocket socket;
private final ReentrantLock lock = new ReentrantLock();
private final Condition ready = lock.newCondition();
private boolean isReady = true;
private final DelegateCompleter completer = new DelegateCompleter();
private CommandSpec spec;
public static void main(String[] args) {
new CommandLine(new Client()).execute(args);
}
public Client() {
super(new Menu());
}
@Override
public void run() {
try {
update();
Parser parser = new DefaultParser();
try (Terminal terminal = TerminalBuilder.builder().build()) {
this.reader = LineReaderBuilder.builder()
.terminal(terminal)
.completer(completer)
.parser(parser)
.build();
String prompt = "> ";
String line;
while (true) {
lock.lock();
try {
while (!isReady) {
ready.await();
}
} finally {
lock.unlock();
}
try {
line = reader.readLine(prompt).trim();
terminal.flush();
if (line.isEmpty() || line.startsWith("#")) continue;
if (line.equals("exit") || line.equals("quit")) break;
var parsedLine = parser.parse(line, 0);
execute(() -> execute(spec, parsedLine));
terminal.flush();
} catch (UserInterruptException e) {
// ignore
} catch (EndOfFileException e) {
break;
} catch (Exception e) {
e.printStackTrace(reader.getTerminal().writer());
terminal.flush();
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
if (socket != null && socket.isOpen()) {
socket.close(CloseFrame.GOING_AWAY);
}
shutdownNow();
}
@Override
protected void handleError(Throwable t) {
t.printStackTrace();
System.exit(1);
}
@Override
protected void onTransition(ClientState from, ClientState to) {
update(to);
}
//<editor-fold desc="Socket" defaultstate="collapsed">
public void setSocket(ClientSocket socket) {
this.socket = socket;
if (socket != null) {
this.socket.setAttachment(this);
}
}
public void onOpen() {
execute(s -> s.onOpen(this));
}
public void onMessage(ServerMessage message) {
if (verbose) {
println(message.toString());
}
execute(s -> s.onMessage(this, message));
}
public void onClose(int code, String reason, boolean remote) {
execute(s -> s.onClose(this, code, reason, remote));
}
public void send(ClientMessage message) {
getSocket().send(message.toString());
}
//</editor-fold>
//<editor-fold desc="Output" defaultstate="collapsed">
public void println(String str) {
reader.printAbove(str);
}
public void println(Object object) {
println(Objects.toString(object));
}
public void printfln(String format, Object...args) {
reader.printAbove(format.formatted(args));
}
public void print(String[]...columns) {
print(null, null, columns);
}
public void print(String prefix, String suffix, String[]...columns) {
int[] length = new int[columns.length];
for (int i = 0; i < columns.length; i++) {
length[i] = 0;
for (int j = 0; j < columns[i].length; j++) {
if (columns[i][j] == null) columns[i][j] = "null";
var str = columns[i][j].length();
if (str > length[i]) {
length[i] = str;
}
}
}
String format = IntStream.range(0, length.length)
.mapToObj(i -> "%" + (i + 1) + "$" + (length[i]) + "s")
.collect(Collectors.joining(" "));
StringBuilder builder = new StringBuilder();
if (prefix != null) {
builder.append(prefix).append('\n');
}
String[] row = new String[columns.length];
for (int j = 0; j < columns[0].length; j++) {
for (int i = 0; i < columns.length; i++) {
row[i] = columns[i][j];
}
builder.append(format.formatted((Object[]) row)).append('\n');
}
if (suffix != null) {
builder.append(suffix);
}
println(builder.toString());
}
//</editor-fold>
public void ready() {
lock.lock();
try {
isReady = true;
ready.signalAll();
} finally {
lock.unlock();
}
}
public void waitForReady() {
lock.lock();
try {
isReady = false;
} finally {
lock.unlock();
}
}
private void update() {
update(getState());
}
private void update(ClientState state) {
synchronized (completer) {
var commandLine = new CommandLine(state.getCommand(), new StateAwareFactory(this, state));
var spec = commandLine.getCommandSpec();
if (this.spec != spec) {
this.spec = spec;
completer.setDelegate(new PicocliJLineCompleter(spec));
}
}
}
private Optional<ClientState> execute(CommandSpec spec, ParsedLine line) {
var commandLine = spec.commandLine();
AtomicReference<Exception> exceptionRef = new AtomicReference<>();
commandLine.setExecutionExceptionHandler((ex, cl, parseResult) -> {
exceptionRef.set(ex);
return -1;
});
commandLine.execute(line.words().toArray(String[]::new));
Exception exception = exceptionRef.get();
if (exception == null) {
Object result;
ParseResult parseResult = commandLine.getParseResult();
if (parseResult.subcommand() != null) {
CommandLine sub = parseResult.subcommand().commandSpec().commandLine();
result = sub.getExecutionResult();
} else {
result = commandLine.getExecutionResult();
}
if (result instanceof ClientState state) {
return Optional.of(state);
} else if (result instanceof Optional) {
//noinspection unchecked
return (Optional<ClientState>) result;
} else {
return Optional.empty();
}
} else {
handleError(exception);
return Optional.empty();
}
}
}

View File

@ -0,0 +1,45 @@
package eu.jonahbauer.wizard.client.cli;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.handshake.ServerHandshake;
import javax.net.ssl.SSLSocketFactory;
import java.net.URI;
public class ClientSocket extends WebSocketClient {
public ClientSocket(URI serverUri) {
super(serverUri);
if ("wss".equals(getURI().getScheme())) {
setSocketFactory(SSLSocketFactory.getDefault());
}
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
getClient().onOpen();
}
@Override
public void onMessage(String s) {
ServerMessage message = ServerMessage.parse(s);
getClient().onMessage(message);
}
@Override
public void onClose(int i, String s, boolean b) {
getClient().onClose(i, s, b);
}
@Override
public void onError(Exception e) {
getClient().println(e.getMessage());
close(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
}
private Client getClient() {
return getAttachment();
}
}

View File

@ -0,0 +1,95 @@
package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.state.Game;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.player.JuggleMessage;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.stream.Stream;
import static picocli.CommandLine.*;
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class, ShowCommand.class})
public class GameCommand {
private final Client client;
public GameCommand(Client client) {
this.client = client;
}
@Command(name = "play")
public void play(
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = HandCompletion.class) Card card
) {
this.client.send(new InteractionMessage(new PlayCardMessage(card)));
this.client.waitForReady();
}
@Command(name = "predict")
public void predict(
@Parameters(index = "0", paramLabel = "<prediction>") int prediction
) {
this.client.send(new InteractionMessage(new PredictMessage(prediction)));
this.client.waitForReady();
}
@Command(name = "trump")
public void trump(
@Parameters(index = "0", paramLabel = "<suit>") Card.Suit suit
) {
this.client.send(new InteractionMessage(new PickTrumpMessage(suit)));
this.client.waitForReady();
}
@Command(name = "juggle")
public void juggle(
@Parameters(index = "0", paramLabel = "<card>", completionCandidates = JuggleCompletion.class) Card card
) {
this.client.send(new InteractionMessage(new JuggleMessage(card)));
this.client.waitForReady();
}
public static class HandCompletion implements Iterable<String> {
private final Game game;
public HandCompletion(Game game) {
this.game = game;
}
@NotNull
@Override
public Iterator<String> iterator() {
return game.getHands().get(game.getSelf()).stream().flatMap(c -> {
if (c == Card.CLOUD) {
return Stream.of(Card.CLOUD_BLUE, Card.CLOUD_GREEN, Card.CLOUD_RED, Card.CLOUD_YELLOW);
} else if (c == Card.JUGGLER) {
return Stream.of(Card.JUGGLER_BLUE, Card.JUGGLER_GREEN, Card.JUGGLER_RED, Card.JUGGLER_YELLOW);
} else if (c == Card.CHANGELING) {
return Stream.of(Card.CHANGELING_JESTER, Card.CHANGELING_WIZARD);
} else {
return Stream.of(c);
}
}).map(Card::toString).iterator();
}
}
public static class JuggleCompletion implements Iterable<String> {
private final Game game;
public JuggleCompletion(Game game) {
this.game = game;
}
@NotNull
@Override
public Iterator<String> iterator() {
return game.getHands().get(game.getSelf()).stream().map(Card::toString).iterator();
}
}
}

View File

@ -0,0 +1,112 @@
package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.state.AwaitingJoinSession;
import eu.jonahbauer.wizard.client.cli.state.Lobby;
import eu.jonahbauer.wizard.client.cli.state.Menu;
import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.RejoinMessage;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.Getter;
import org.ajbrown.namemachine.NameGenerator;
import org.java_websocket.framing.CloseFrame;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.UUID;
import static picocli.CommandLine.*;
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
public class LobbyCommand {
@Getter(lazy = true)
private static final NameGenerator nameGenerator = new NameGenerator();
private final Client client;
private final Lobby lobby;
public LobbyCommand(Client client, Lobby lobby) {
this.client = client;
this.lobby = lobby;
}
@Command(name = "list", description = "Shows a list of available sessions")
public void list() {
var sessions = lobby.getSessions();
var count = sessions.size();
if (count > 0) {
StringBuilder builder = new StringBuilder();
if (count == 1) builder.append("There is one open session:\n");
else builder.append("There are ").append(count).append(" open sessions:\n");
sessions.forEach((uuid, session) -> builder.append(session).append('\n'));
client.println(builder);
} else {
client.println("No sessions.");
}
}
@Command(name = "disconnect", description = "Disconnects from the server")
public Menu disconnect() {
client.getSocket().close(CloseFrame.GOING_AWAY);
client.setSocket(null);
return new Menu();
}
@Command(name = "join", description = "Joins the specified session")
public AwaitingJoinSession join(
@Parameters(index = "0", paramLabel = "<session>", description = "session uuid", completionCandidates = SessionCompleter.class) UUID session,
@Option(names = "--name", description = "user name") String userName
) {
if (userName == null) {
userName = getNameGenerator().generateName().getFirstName();
client.println("Your name will be " + userName + ".");
}
client.send(new JoinSessionMessage(session, userName));
return new AwaitingJoinSession();
}
@Command(name = "create")
public AwaitingJoinSession create(
@Parameters(index = "0", paramLabel = "<session>", description = "human readable session name") String sessionName,
@Option(names = "--name", description = "user name") String userName,
@Option(names = "--configuration", description = "game configuration", defaultValue = "DEFAULT") Configuration configuration,
@Option(names = "--timeout", description = "interaction timeout", defaultValue = "60000") long timeout
) {
if (userName == null) {
userName = getNameGenerator().generateName().getFirstName();
client.println("Your name will be " + userName + ".");
}
client.send(new CreateSessionMessage(sessionName, userName, timeout, configuration));
return new AwaitingJoinSession();
}
@Command(name = "rejoin")
public AwaitingJoinSession rejoin(
@Parameters(index = "0", paramLabel = "<session>", description = "session uuid", completionCandidates = SessionCompleter.class) UUID session,
@Parameters(index = "1", paramLabel = "<player>", description = "player uuid") UUID player,
@Parameters(index = "2", paramLabel = "<secret>", description = "player secret") String secret
) {
client.send(new RejoinMessage(session, player, secret));
return new AwaitingJoinSession();
}
public static class SessionCompleter implements Iterable<String> {
private final Lobby lobby;
private SessionCompleter(Lobby lobby) {
this.lobby = lobby;
}
@NotNull
@Override
public Iterator<String> iterator() {
return lobby.getSessions().values().stream().map(SessionData::getUuid).map(UUID::toString).iterator();
}
}
}

View File

@ -0,0 +1,28 @@
package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.ClientSocket;
import eu.jonahbauer.wizard.client.cli.state.AwaitingConnection;
import java.net.URI;
import static picocli.CommandLine.*;
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
public class MenuCommand {
private final Client client;
public MenuCommand(Client client) {
this.client = client;
}
@Command(name = "connect", description = "Connects to the specified server")
public AwaitingConnection connect(
@Parameters(index = "0", paramLabel = "<uri>", description = "server uri") URI uri
) {
ClientSocket socket = new ClientSocket(uri);
client.setSocket(socket);
socket.connect();
return new AwaitingConnection();
}
}

View File

@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.client.cli.commands;
import static picocli.CommandLine.Command;
@Command(name = "quit", aliases = "exit")
public class QuitCommand {
}

View File

@ -0,0 +1,52 @@
package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.state.AwaitingJoinLobby;
import eu.jonahbauer.wizard.client.cli.state.Session;
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.ReadyMessage;
import static picocli.CommandLine.*;
@Command(name = "\b", subcommands = {HelpCommand.class, QuitCommand.class})
public class SessionCommand {
private final Client client;
private final Session session;
public SessionCommand(Client client, Session session) {
this.client = client;
this.session = session;
}
@Command(name = "ready")
public void ready(
@Parameters(index = "0", paramLabel = "<ready>", defaultValue = "true") boolean ready
) {
session.setNextReady(ready);
client.send(new ReadyMessage(ready));
client.waitForReady();
}
@Command(name = "leave", description = "Leaves the current session and returns to the lobby")
public AwaitingJoinLobby leave() {
client.send(new LeaveSessionMessage());
return new AwaitingJoinLobby();
}
@Command(name = "list", description = "Shows a list of players in the current session")
public void list() {
var players = session.getPlayers();
var count = players.size();
if (count > 0) {
StringBuilder builder = new StringBuilder();
if (count == 1) builder.append("There is one player in this session:\n");
else builder.append("There are ").append(count).append(" players in this session:\n");
players.forEach((id, session) -> builder.append(session).append('\n'));
client.println(builder);
} else {
client.println("There are no players in this session.");
}
}
}

View File

@ -0,0 +1,78 @@
package eu.jonahbauer.wizard.client.cli.commands;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.state.Game;
import eu.jonahbauer.wizard.client.cli.util.Pair;
import eu.jonahbauer.wizard.common.model.Card;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import static picocli.CommandLine.Command;
@Command(name = "show")
public class ShowCommand {
private final Client client;
private final Game game;
public ShowCommand(Client client, Game game) {
this.client = client;
this.game = game;
}
@Command(name = "players")
public void players() {
StringBuilder builder = new StringBuilder();
for (Map.Entry<UUID, String> player : game.getPlayers().entrySet()) {
builder.append(player.getValue()).append('\t').append(player.getKey()).append('\n');
}
client.println(builder.toString());
}
@Command(name = "stack")
public void stack() {
var stack = game.getStack();
String[] col0 = new String[stack.size()];
String[] col1 = new String[stack.size()];
int i = 0;
for (Pair<UUID, Card> entry : game.getStack()) {
col0[i] = Objects.toString(entry.getValue());
col1[i] = Objects.toString(game.nameOf(entry.getKey()));
i++;
}
client.print(col0, col1);
}
@Command(name = "hand")
public void hand() {
client.println(game.getHands().get(game.getSelf()));
}
@Command(name = "predictions", aliases = "tricks")
public void predictions() {
var players = game.getPlayers().keySet();
String[] col0 = new String[players.size() + 1];
String[] col1 = new String[players.size() + 1];
String[] col2 = new String[players.size() + 1];
col0[0] = "";
col1[0] = "Prediction";
col2[0] = "Tricks";
int i = 1;
for (UUID player : players) {
col0[i] = game.nameOf(player);
col1[i] = Objects.toString(game.getPredictions().get(player));
col2[i] = Objects.toString(game.getTricks().getOrDefault(player, List.of()).stream().filter(l -> !l.contains(Card.BOMB)).count());
i++;
}
client.print(col0, col1, col2);
}
@Command(name = "trump")
public void trump() {
client.printfln("%s (%s)", game.getTrumpSuit(), game.getTrumpCard());
}
}

View File

@ -0,0 +1,35 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.SneakyThrows;
import picocli.CommandLine.Model.CommandSpec;
import java.util.Optional;
public abstract class Awaiting extends BaseState implements ClientState {
private static final CommandSpec COMMAND_SPEC = CommandSpec.create();
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
return unexpectedMessage(client, message);
}
@Override
@SneakyThrows
public Optional<ClientState> onEnter(Client client) {
client.waitForReady();
client.timeout(this, 10_000);
return Optional.empty();
}
@Override
public Optional<ClientState> onTimeout(Client client) {
client.println("Timed out. Returning to menu");
return Optional.of(new Menu());
}
@Override
public Object getCommand() {
return COMMAND_SPEC;
}
}

View File

@ -0,0 +1,26 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import java.util.Optional;
public final class AwaitingConnection extends Awaiting {
@Override
public Optional<ClientState> onEnter(Client client) {
client.println("Awaiting connection...");
return super.onEnter(client);
}
@Override
public Optional<ClientState> onOpen(Client client) {
client.println("Connection established.");
return Optional.of(new AwaitingJoinLobby());
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
client.printfln("Connection could not be established. (code=%d, reason=%s)", code, reason);
return Optional.of(new Menu());
}
}

View File

@ -0,0 +1,25 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionListMessage;
import java.util.Optional;
public final class AwaitingJoinLobby extends Awaiting {
@Override
public Optional<ClientState> onEnter(Client client) {
client.println("Waiting for session list...");
return super.onEnter(client);
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionListMessage list) {
return Optional.of(new Lobby(list.getSessions()));
} else {
return super.onMessage(client, message);
}
}
}

View File

@ -0,0 +1,39 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.messages.server.*;
import java.util.Optional;
public final class AwaitingJoinSession extends Awaiting {
@Override
public Optional<ClientState> onEnter(Client client) {
client.println("Waiting for acknowledgment...");
return super.onEnter(client);
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionJoinedMessage joined) {
return Optional.of(new Session(joined));
} else if (message instanceof NackMessage nack) {
switch (nack.getCode()) {
case NackMessage.GAME_ALREADY_STARTED -> client.println("Error: Game has already started.");
case NackMessage.SESSION_FULL -> client.println("Error: The session is full.");
case NackMessage.SESSION_NOT_FOUND -> client.println("Error: Session not found.");
case NackMessage.PLAYER_NAME_TAKEN -> client.println("Player name already taken.");
case NackMessage.PLAYER_NAME_NOT_ALLOWED -> client.println("Player name not allowed.");
case NackMessage.SESSION_NAME_TAKEN -> client.println("Session name already taken.");
case NackMessage.SESSION_NAME_NOT_ALLOWED -> client.println("Session name not allowed.");
default -> client.println("Nack " + nack.getCode() + ": " + nack.getMessage());
}
return Optional.of(new AwaitingJoinLobby());
} else if (message instanceof SessionModifiedMessage || message instanceof SessionRemovedMessage) {
// drop
return Optional.empty();
} else {
return super.onMessage(client, message);
}
}
}

View File

@ -0,0 +1,39 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import java.util.Optional;
public abstract class BaseState implements ClientState {
@Override
public Optional<ClientState> onEnter(Client context) {
context.ready();
return ClientState.super.onEnter(context);
}
@Override
public Optional<ClientState> onOpen(Client client) {
throw new IllegalStateException();
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
if (remote) {
client.printfln("Lost connection (code=%d, reason=%s).", code, reason);
} else {
client.printfln("Connection closed (code=%d, reason=%s).", code, reason);
}
return Optional.of(new Menu());
}
protected static Optional<ClientState> unexpectedMessage(Client client, ServerMessage message) {
// return to menu on unexpected message
client.println("Fatal: Unexpected message " + message + ". Returning to menu.");
if (client.isVerbose()) {
new Exception().printStackTrace();
}
return Optional.of(new Menu());
}
}

View File

@ -0,0 +1,17 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.common.machine.TimeoutState;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import java.util.Optional;
public interface ClientState extends TimeoutState<ClientState, Client> {
Optional<ClientState> onOpen(Client client);
Optional<ClientState> onMessage(Client client, ServerMessage message);
Optional<ClientState> onClose(Client client, int code, String reason, boolean remote);
Object getCommand();
}

View File

@ -0,0 +1,208 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.commands.GameCommand;
import eu.jonahbauer.wizard.client.cli.util.Pair;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.messages.player.ContinueMessage;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
@Getter
public final class Game extends BaseState {
private final UUID self;
private final UUID session;
private final String secret;
private final Map<UUID, String> players;
private final Map<UUID, Integer> scores = new HashMap<>();
private int round = -1;
private final Map<UUID, Integer> predictions = new HashMap<>();
private final Map<UUID, List<Card>> hands = new HashMap<>();
private final Map<UUID, List<List<Card>>> tricks = new HashMap<>();
private int trick = -1;
private final List<Pair<UUID, Card>> stack = new ArrayList<>();
private Card trumpCard;
private Card.Suit trumpSuit;
public Game(UUID self, UUID session, String secret, Map<UUID, String> players) {
this.self = self;
this.session = session;
this.secret = secret;
this.players = players;
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof GameMessage game) {
var observerMessage = game.getObserverMessage();
if (observerMessage instanceof StateMessage state) {
switch (state.getState()) {
case "starting_round" -> {
client.printfln("Round %d is starting...", ++round);
predictions.clear();
tricks.clear();
trumpSuit = null;
trumpCard = null;
stack.clear();
trick = -1;
}
case "starting_trick" -> {
client.printfln("Trick %d is starting...", ++trick);
stack.clear();
}
case "finished" -> {
client.println("The game has finished.");
return returnToSession();
}
case "error" -> {
client.println("The game has finished with an error.");
return returnToSession();
}
}
} else if (observerMessage instanceof HandMessage hand) {
hands.put(hand.getPlayer(), new ArrayList<>(hand.getHand()));
if (hand.getPlayer().equals(self)) {
client.printfln("Your hand cards are: %s", hand.getHand());
} else {
client.printfln("%s's hand cards are: %s", nameOf(hand.getPlayer()), hand.getHand());
}
} else if (observerMessage instanceof PredictionMessage prediction) {
predictions.put(prediction.getPlayer(), prediction.getPrediction());
if (prediction.getPlayer().equals(self)) {
client.printfln("You predicted: %d", prediction.getPrediction());
} else {
client.printfln("%s predicted: %d", nameOf(prediction.getPlayer()), prediction.getPrediction());
}
} else if (observerMessage instanceof TrumpMessage trump) {
trumpCard = trump.getCard();
trumpSuit = trump.getSuit();
if (trumpCard == null) {
client.println("There is no trump in this round.");
} else {
client.printfln("The trump suit is %s (%s).", trumpSuit, trumpCard);
}
} else if (observerMessage instanceof TrickMessage trick) {
this.stack.clear();
this.tricks.computeIfAbsent(trick.getPlayer(), player -> new ArrayList<>())
.add(trick.getCards());
client.printfln("This trick %s goes to %s.", trick.getCards(), nameOf(trick.getPlayer()));
} else if (observerMessage instanceof CardMessage card) {
this.stack.add(Pair.of(card.getPlayer(), card.getCard()));
var hand = this.hands.get(card.getPlayer());
if (hand != null) {
switch (card.getCard()) {
case CHANGELING_JESTER, CHANGELING_WIZARD -> hand.remove(Card.CHANGELING);
case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> hand.remove(Card.JUGGLER);
case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> hand.remove(Card.CLOUD);
default -> hand.remove(card.getCard());
}
}
if (card.getPlayer().equals(self)) {
client.printfln("You played %s.", card.getCard());
} else {
client.printfln("%s played %s.", nameOf(card.getPlayer()), card.getCard());
}
} else if (observerMessage instanceof ScoreMessage score) {
score.getPoints().forEach((player, points) -> scores.merge(player, points, Integer::sum));
String[] col0 = new String[players.size()];
String[] col1 = new String[players.size()];
int i = 0;
for (UUID player : players.keySet()) {
col0[i] = nameOf(player);
col1[i] = Objects.toString(scores.getOrDefault(player, 0));
i++;
}
client.print("The scores are as follows:", "", col0, col1);
} else if (observerMessage instanceof UserInputMessage input) {
if (input.getAction() == UserInputMessage.Action.SYNC) {
client.send(new InteractionMessage(new ContinueMessage()));
} else if (self.equals(input.getPlayer())) {
client.printfln("It is your turn to %s. You have time until %s.", switch (input.getAction()) {
case CHANGE_PREDICTION -> "change your prediction";
case JUGGLE_CARD -> "juggle a card";
case PLAY_CARD -> "play a card";
case PICK_TRUMP -> "pick the trump suit";
case MAKE_PREDICTION -> "make a prediction";
default -> throw new AssertionError();
}, LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()), ZoneId.systemDefault()));
} else {
client.printfln(
"Waiting for input %s from %s. (times out at %s)",
input.getAction(),
nameOf(input.getPlayer()),
LocalDateTime.ofInstant(Instant.ofEpochMilli(input.getTimeout()),
ZoneId.systemDefault()
)
);
}
} else if (observerMessage instanceof TimeoutMessage) {
client.println("Timed out.");
} else {
throw new AssertionError("Unknown observer message " + observerMessage.getClass().getSimpleName() + "");
}
return Optional.empty();
} else if (message instanceof NackMessage nack) {
int code = nack.getCode();
if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
client.println("Error: " + nack.getMessage());
client.ready();
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
} else if (message instanceof AckMessage) {
client.ready();
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
public String nameOf(UUID player) {
if (player == null) {
return "all players";
} else {
return players.get(player);
}
}
@Override
public Object getCommand() {
return GameCommand.class;
}
private Optional<ClientState> returnToSession() {
return Optional.of(new Session(
self,
session,
secret,
false,
players.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> new PlayerData(entry.getKey(), entry.getValue(), false)
))
));
}
}

View File

@ -0,0 +1,55 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.commands.LobbyCommand;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.server.*;
import lombok.Getter;
import java.util.*;
public final class Lobby extends BaseState {
@Getter
private final Map<UUID, SessionData> sessions = new HashMap<>();
public Lobby(Collection<SessionData> sessions) {
sessions.forEach(session -> this.sessions.put(session.getUuid(), session));
}
@Override
public Optional<ClientState> onEnter(Client client) {
if (sessions.size() == 1) {
client.println("Successfully joined the server. There is one open session.");
} else {
client.printfln("Successfully joined the server. There are %d open sessions.", sessions.size());
}
return super.onEnter(client);
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionCreatedMessage created) {
var session = created.getSession();
sessions.put(session.getUuid(), session);
return Optional.empty();
} else if (message instanceof SessionRemovedMessage removed) {
sessions.remove(removed.getSession());
return Optional.empty();
} else if (message instanceof SessionModifiedMessage modified) {
var session = modified.getSession();
sessions.put(session.getUuid(), session);
return Optional.empty();
} else if (message instanceof SessionListMessage list) {
sessions.clear();
list.getSessions().forEach(session -> sessions.put(session.getUuid(), session));
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
@Override
public Object getCommand() {
return LobbyCommand.class;
}
}

View File

@ -0,0 +1,42 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.commands.MenuCommand;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.SneakyThrows;
import org.java_websocket.framing.CloseFrame;
import java.util.Optional;
public final class Menu extends BaseState {
@Override
@SneakyThrows
public Optional<ClientState> onEnter(Client client) {
if (client.getSocket() != null && client.getSocket().isOpen()) {
client.getSocket().close(CloseFrame.GOING_AWAY);
client.waitForReady();
return Optional.empty();
}
return super.onEnter(client);
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
// it is possible that there are messages still queued after
// returning to the menu as a result of a previous message
return Optional.empty();
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
client.ready();
super.onClose(client, code, reason, remote);
return Optional.empty();
}
@Override
public Object getCommand() {
return MenuCommand.class;
}
}

View File

@ -0,0 +1,99 @@
package eu.jonahbauer.wizard.client.cli.state;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.commands.SessionCommand;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.server.*;
import lombok.Getter;
import lombok.Setter;
import picocli.CommandLine.Model.CommandSpec;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Getter
public final class Session extends BaseState {
private static final CommandSpec COMMAND_SPEC = CommandSpec.forAnnotatedObject(new SessionCommand(null, null));
private final UUID self;
private final UUID session;
private final String secret;
private final Map<UUID, PlayerData> players = new HashMap<>();
private boolean ready;
@Setter
private Boolean nextReady;
public Session(SessionJoinedMessage joined) {
this.self = joined.getPlayer();
this.session = joined.getSession();
this.ready = false;
this.secret = joined.getSecret();
joined.getPlayers().forEach(player -> players.put(player.getUuid(), player));
}
public Session(UUID self, UUID session, String secret, boolean ready, Map<UUID, PlayerData> players) {
this.self = self;
this.session = session;
this.secret = secret;
this.players.putAll(players);
this.ready = ready;
}
@Override
public Optional<ClientState> onEnter(Client client) {
if (players.size() - 1 == 1) {
client.printfln("Successfully joined session %s. There is one other player.", session);
} else {
client.printfln("Successfully joined session %s. There are %d other players.", session, players.size() - 1);
}
client.printfln("You are %s. Your secret is %s", self, secret);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof PlayerJoinedMessage join) {
var player = join.getPlayer();
players.put(player.getUuid(), player);
client.printfln("Player \"%s\" joined the session.", player.getName());
return Optional.empty();
} else if (message instanceof PlayerLeftMessage leave) {
var player = players.remove(leave.getPlayer());
client.printfln("Player \"%s\" left the session.", player.getName());
return Optional.empty();
} else if (message instanceof PlayerModifiedMessage modified) {
var player = modified.getPlayer();
players.put(player.getUuid(), player);
client.printfln("Player \"%s\" was modified.", player.getName());
return Optional.empty();
} else if (message instanceof StartingGameMessage) {
return Optional.of(new Game(
self,
session,
secret,
players.values().stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName))
));
} else if (nextReady != null && message instanceof NackMessage nack) {
client.println("Error: " + nack.getMessage());
nextReady = null;
client.ready();
return Optional.empty();
} else if (nextReady != null && message instanceof AckMessage) {
ready = nextReady;
nextReady = null;
client.ready();
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
@Override
public Object getCommand() {
return SessionCommand.class;
}
}

View File

@ -0,0 +1,13 @@
package eu.jonahbauer.wizard.client.cli.util;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Delegate;
import org.jline.reader.Completer;
@Getter
@Setter
public class DelegateCompleter implements Completer {
@Delegate
private Completer delegate;
}

View File

@ -0,0 +1,24 @@
package eu.jonahbauer.wizard.client.cli.util;
import java.util.Map;
public record Pair<F,S>(F first, S second) implements Map.Entry<F, S> {
public static <F,S> Pair<F,S> of(F first, S second) {
return new Pair<>(first, second);
}
@Override
public F getKey() {
return first();
}
@Override
public S getValue() {
return second();
}
@Override
public S setValue(S value) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,50 @@
package eu.jonahbauer.wizard.client.cli.util;
import eu.jonahbauer.wizard.client.cli.Client;
import eu.jonahbauer.wizard.client.cli.state.ClientState;
import picocli.CommandLine;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
public class StateAwareFactory implements CommandLine.IFactory {
private final CommandLine.IFactory defaultFactory = CommandLine.defaultFactory();
private final Client client;
private final ClientState clientState;
public StateAwareFactory(Client client, ClientState clientState) {
this.client = client;
this.clientState = clientState;
}
@Override
@SuppressWarnings("unchecked")
public <K> K create(Class<K> cls) throws Exception {
try {
return defaultFactory.create(cls);
} catch (Exception e) {
c: for (Constructor<K> constructor : (Constructor<K>[]) cls.getDeclaredConstructors()) {
Object[] values = new Object[constructor.getParameterCount()];
Parameter[] parameters = constructor.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
if (ClientState.class.isAssignableFrom(parameter.getType())) {
values[i] = clientState;
} else if (Client.class.isAssignableFrom(parameter.getType())) {
values[i] = client;
} else {
continue c;
}
}
constructor.setAccessible(true);
return constructor.newInstance(values);
}
var e2 = new NoSuchMethodException("Could not find a suitable constructor for class " + cls.getName());
e2.addSuppressed(e);
throw e2;
}
}
}

View File

@ -0,0 +1,20 @@
project(":wizard-client:wizard-client-libgdx:desktop") {
apply(plugin = "java-library")
dependencies {
implementation(project(":wizard-client:wizard-client-libgdx:core"))
api(LibGDX.backend_lwjgl3)
api(LibGDX.platform_desktop)
}
}
project(":wizard-client:wizard-client-libgdx:core") {
apply(plugin = "java-library")
dependencies {
api(LibGDX.api)
implementation(project(":wizard-common"))
implementation(JavaWebSocket.id)
}
}

View File

@ -0,0 +1,22 @@
val texturePackerSource = "src/main/textures"
val texturePackerResources = "$buildDir/generated/sources/texturePacker/resources/main"
val texturePackerGeneratedSources = "$buildDir/generated/sources/texturePacker/java/main"
sourceSets.main.get().java.srcDir(texturePackerGeneratedSources)
sourceSets.main.get().resources.srcDir(texturePackerResources)
tasks {
val packTextures = register<TexturePackerTask>("packTextures") {
input.set(file(texturePackerSource))
resourceOutput.set(file(texturePackerResources))
generatedSourceOutput.set(file(texturePackerGeneratedSources))
}
processResources {
dependsOn(packTextures)
}
compileJava {
dependsOn(packTextures)
}
}

View File

@ -0,0 +1,96 @@
package eu.jonahbauer.wizard.client.libgdx;
import eu.jonahbauer.wizard.client.libgdx.screens.ErrorScreen;
import eu.jonahbauer.wizard.client.libgdx.state.ClientState;
import eu.jonahbauer.wizard.client.libgdx.state.Menu;
import eu.jonahbauer.wizard.common.machine.TimeoutContext;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import java.util.Optional;
import java.util.function.BiFunction;
@Log4j2
public class Client extends TimeoutContext<ClientState, Client> {
@Getter
private final WizardGame game;
@Getter
private ClientSocket socket;
@Getter
@Setter
private boolean error = false;
public Client(WizardGame game) {
super(new Menu());
this.game = game;
}
@Override
protected void handleError(Throwable t) {
// TODO better error handling
log.error("An error occurred.", t);
game.setScreen(new ErrorScreen(game, t.getMessage()));
error = true;
var menu = new Menu();
forceTransition(menu);
menu.onEnter(this);
}
public void setSocket(ClientSocket socket) {
this.socket = socket;
if (socket != null) {
this.socket.setAttachment(this);
}
}
public void onOpen() {
try {
execute(s -> s.onOpen(this));
} catch (Throwable t) {
handleError(t);
}
}
public void onClose(int code, String reason, boolean remote) {
try {
execute(s -> s.onClose(this, code, reason, remote));
} catch (Throwable t) {
handleError(t);
}
}
public void onMessage(ServerMessage message) {
try {
execute(s -> s.onMessage(this, message));
} catch (Throwable t) {
handleError(t);
}
}
@Override
protected void onTransition(ClientState from, ClientState to) {
log.debug("Transition from {} to {}.", from.getClass().getSimpleName(), to.getClass().getSimpleName());
}
public void send(ClientMessage message) {
getSocket().send(message.toString());
}
public <T extends ClientState> void execute(Class<T> stateClass, BiFunction<T, Client, Optional<ClientState>> transition) {
execute(s -> {
if (stateClass.isInstance(s)) {
return transition.apply(stateClass.cast(s), this);
} else {
return Optional.empty();
}
});
}
}

View File

@ -0,0 +1,56 @@
package eu.jonahbauer.wizard.client.libgdx;
import com.badlogic.gdx.Gdx;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.extern.log4j.Log4j2;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.handshake.ServerHandshake;
import javax.net.ssl.SSLSocketFactory;
import java.net.URI;
@Log4j2
public class ClientSocket extends WebSocketClient {
public ClientSocket(URI serverUri) {
super(serverUri);
if ("wss".equals(getURI().getScheme())) {
setSocketFactory(SSLSocketFactory.getDefault());
}
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
Gdx.app.postRunnable(() -> getClient().onOpen());
}
@Override
public void onMessage(String s) {
log.debug("Received: {}", s);
ServerMessage message = ServerMessage.parse(s);
Gdx.app.postRunnable(() -> getClient().onMessage(message));
}
@Override
public void onClose(int i, String s, boolean b) {
Gdx.app.postRunnable(() -> getClient().onClose(i, s, b));
}
@Override
public void onError(Exception e) {
log.error("", e);
close(CloseFrame.ABNORMAL_CLOSE, e.getMessage());
}
@Override
public void send(String text) {
log.debug("Sending: {}", text);
super.send(text);
}
private Client getClient() {
return getAttachment();
}
}

View File

@ -0,0 +1,88 @@
package eu.jonahbauer.wizard.client.libgdx;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Graphics;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import eu.jonahbauer.wizard.client.libgdx.screens.MainMenuScreen;
import eu.jonahbauer.wizard.client.libgdx.util.SavedData;
import eu.jonahbauer.wizard.client.libgdx.util.WizardAssetManager;
import lombok.Getter;
public class WizardGame extends Game {
public static final boolean DEBUG = false;
public static final int WIDTH = 1920;
public static final int HEIGHT = 1080;
public SpriteBatch batch;
public WizardAssetManager assets;
public final SavedData storage = new SavedData();
private boolean fullscreenToggle;
private int oldHeight, oldWidth;
@Getter
private final Client client = new Client(this);
@Override
public void create() {
batch = new SpriteBatch();
assets = new WizardAssetManager();
assets.loadShared();
assets.finishLoading();
// background music
Music backgroundMusic = assets.get(WizardAssetManager.MUSIC_BACKGROUND, Music.class);
backgroundMusic.setLooping(true);
backgroundMusic.setVolume(0.07f);
backgroundMusic.play();
// set cursor
Pixmap cursor = assets.get(WizardAssetManager.CURSOR, Pixmap.class);
Gdx.graphics.setCursor(Gdx.graphics.newCursor(cursor, 0, 0));
assets.unload(WizardAssetManager.CURSOR);
this.setScreen(new MainMenuScreen(this));
}
@Override
public void render() {
super.render();
// alt + enter shortcut for fullscreen
var enter = Gdx.input.isKeyPressed(Input.Keys.ENTER);
var alt = (Gdx.input.isKeyPressed(Input.Keys.ALT_LEFT) || Gdx.input.isKeyPressed(Input.Keys.ALT_RIGHT));
var toggle = enter && alt;
if (toggle && !this.fullscreenToggle) {
this.fullscreenToggle = true;
var fullscreen = Gdx.graphics.isFullscreen();
Graphics.DisplayMode displayMode = Gdx.graphics.getDisplayMode();
if (fullscreen) {
Gdx.graphics.setWindowedMode(oldWidth, oldHeight);
} else {
oldWidth = Gdx.graphics.getWidth();
oldHeight = Gdx.graphics.getHeight();
Gdx.graphics.setFullscreenMode(displayMode);
}
} else if (!toggle) {
this.fullscreenToggle = false;
}
}
@Override
public void dispose () {
batch.dispose();
assets.dispose();
client.shutdownNow();
var socket = client.getSocket();
if (socket != null) {
socket.close();
}
}
}

View File

@ -0,0 +1,33 @@
package eu.jonahbauer.wizard.client.libgdx.actions;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.utils.Pools;
import lombok.Setter;
public class ChangeParentAction extends Action {
private final Vector2 pos = Pools.obtain(Vector2.class);
@Setter
private Group parent;
private boolean finished;
public boolean act(float delta) {
if (!finished) {
finished = true;
pos.set(target.getX(), target.getY());
if (target.hasParent()) {
target.getParent().localToStageCoordinates(pos);
}
parent.stageToLocalCoordinates(pos);
parent.addActor(target);
target.setPosition(pos.x, pos.y);
}
return true;
}
public void restart () {
finished = false;
}
}

View File

@ -0,0 +1,41 @@
package eu.jonahbauer.wizard.client.libgdx.actions;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.actions.*;
public class MyActions extends Actions {
public static ChangeParentAction changeParent(Group parent) {
var action = action(ChangeParentAction.class);
action.setParent(parent);
return action;
}
public static SilentlyRemoveActorAction removeActorSilently () {
return action(SilentlyRemoveActorAction.class);
}
public static WaitAction delay(Actor actor) {
var action = action(WaitAction.class);
action.setTarget(actor);
return action;
}
public static void finish(Action action) {
if (action instanceof ParallelAction parallel) {
var subactions = parallel.getActions();
for (int i = 0; i < subactions.size; i++) {
finish(subactions.get(i));
}
} else if (action instanceof DelayAction delay) {
delay.finish();
finish(delay.getAction());
} else if (action instanceof DelegateAction delegate) {
var subaction = delegate.getAction();
finish(subaction);
} else if (action instanceof TemporalAction temporal) {
temporal.finish();
}
}
}

View File

@ -0,0 +1,22 @@
package eu.jonahbauer.wizard.client.libgdx.actions;
import com.badlogic.gdx.scenes.scene2d.Action;
/** Removes an actor from the stage without immediately invalidating its parents. */
public class SilentlyRemoveActorAction extends Action {
private boolean removed;
public boolean act (float delta) {
if (!removed) {
removed = true;
if (target.getParent() != null) {
target.getParent().getChildren().removeValue(target, true);
}
}
return true;
}
public void restart () {
removed = false;
}
}

View File

@ -0,0 +1,10 @@
package eu.jonahbauer.wizard.client.libgdx.actions;
import com.badlogic.gdx.scenes.scene2d.Action;
public class WaitAction extends Action {
@Override
public boolean act(float delta) {
return getTarget().getActions().size == 0;
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
public class ChangePredictionOverlay extends MakePredictionOverlay {
public ChangePredictionOverlay(@NotNull GameScreen gameScreen, long timeout, int round, int oldPrediction) {
super(
gameScreen,
timeout,
oldPrediction == 0 ? new int[] {oldPrediction + 1}
: oldPrediction == round + 1 ? new int[] {oldPrediction - 1}
: new int[] {oldPrediction - 1, oldPrediction + 1}
);
}
@Override
public VerticalGroup createContent() {
var root = super.createContent();
var card = new CardActor(Card.CLOUD, atlas);
root.addActorAt(0, card);
root.padTop(- CardActor.PREF_HEIGHT);
return root;
}
}

View File

@ -0,0 +1,5 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
public interface InteractionOverlay {
void close();
}

View File

@ -0,0 +1,82 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.actors.PadOfTruth;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.stream.IntStream;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.run;
public class MakePredictionOverlay extends Overlay implements InteractionOverlay {
private final int[] values;
private final TextButton[] buttons;
private final @NotNull PadOfTruth padOfTruth;
public MakePredictionOverlay(@NotNull GameScreen gameScreen, long timeout, int round) {
this(gameScreen, timeout, IntStream.range(0, round + 2).toArray());
}
protected MakePredictionOverlay(@NotNull GameScreen gameScreen, long timeout, int[] values) {
super(gameScreen, timeout);
this.values = values;
this.buttons = new TextButton[values.length];
this.padOfTruth = Objects.requireNonNull(gameScreen.getPadOfTruth());
}
@Override
public VerticalGroup createContent() {
var root = new VerticalGroup().columnCenter().space(10);
var prompt = new Label(messages.get("game.overlay.make_prediction.prompt"), skin);
var buttonGroup = new HorizontalGroup().space(20);
var listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (isClosing()) return;
for (int i = 0; i < values.length; i++) {
if (actor == buttons[i]) {
screen.onPredictionMade(values[i]);
break;
}
}
}
};
for (int i = 0; i < values.length; i++) {
buttons[i] = new TextButton(String.valueOf(values[i]), skin);
buttons[i].addListener(listener);
buttonGroup.addActor(buttons[i]);
}
root.addActor(prompt);
root.addActor(buttonGroup);
return root;
}
@Override
protected void show(Group parent) {
super.show(parent);
padOfTruth.addAction(run(() -> screen.getOverlayRoot().addActor(padOfTruth)));
}
@Override
protected void onClosing() {
super.onClosing();
padOfTruth.addAction(run(() -> screen.getContentRoot().addActor(padOfTruth)));
}
}

View File

@ -0,0 +1,86 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Container;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.I18NBundle;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import lombok.AccessLevel;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.run;
public abstract class Overlay extends Action {
protected final GameScreen screen;
protected final Skin skin;
protected final I18NBundle messages;
protected final TextureAtlas atlas;
private long timeout;
private Container<?> root;
private boolean started;
@Getter(AccessLevel.PROTECTED)
private boolean closing;
private boolean closed;
public Overlay(@NotNull GameScreen gameScreen, long timeout) {
this.screen = gameScreen;
this.skin = gameScreen.getSkin();
this.messages = gameScreen.getMessages();
this.atlas = gameScreen.getAtlas();
this.timeout = timeout;
}
@Override
public boolean act(float delta) {
if (!started) {
if (System.currentTimeMillis() > timeout) {
return true;
}
started = true;
show(screen.getOverlayRoot());
}
if (System.currentTimeMillis() > timeout && !closing) {
closing = true;
onClosing();
}
return closed;
}
protected abstract Actor createContent();
protected Container<?> getRoot() {
if (root == null) {
root = new Container<>(createContent());
root.setSize(WizardGame.WIDTH, WizardGame.HEIGHT);
}
return root;
}
protected void show(Group parent) {
parent.addActor(getRoot());
}
protected final void onClosed() {
if (closed) return;
closed = true;
getRoot().remove();
}
protected void onClosing() {
getRoot().addAction(run(this::onClosed));
}
public void close() {
timeout = 0;
}
}

View File

@ -0,0 +1,63 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
import java.util.EnumMap;
public class PickTrumpOverlay extends Overlay implements InteractionOverlay {
private final boolean allowNone;
private final EnumMap<Card.Suit, CardActor> cards = new EnumMap<>(Card.Suit.class);
public PickTrumpOverlay(@NotNull GameScreen gameScreen, long timeout, boolean allowNone) {
super(gameScreen, timeout);
this.allowNone = allowNone;
}
@Override
public Actor createContent() {
var root = new VerticalGroup().columnCenter().space(10);
var prompt = new Label(messages.get("game.overlay.pick_trump.prompt"), skin);
var cardGroup = new HorizontalGroup().space(20);
cards.put(Card.Suit.RED, new CardActor(Card.Suit.RED, atlas));
cards.put(Card.Suit.GREEN, new CardActor(Card.Suit.GREEN, atlas));
cards.put(Card.Suit.BLUE, new CardActor(Card.Suit.BLUE, atlas));
cards.put(Card.Suit.YELLOW, new CardActor(Card.Suit.YELLOW, atlas));
if (allowNone) {
cards.put(Card.Suit.NONE, new CardActor(Card.Suit.NONE, atlas));
}
cards.values().forEach(cardGroup::addActor);
cardGroup.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
if (isClosing()) return;
var target = event.getTarget();
for (Card.Suit suit : Card.Suit.values()) {
if (cards.get(suit) == target) {
screen.onSuitClicked(suit);
break;
}
}
}
});
root.addActor(prompt);
root.addActor(cardGroup);
return root;
}
}

View File

@ -0,0 +1,62 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
import java.util.EnumMap;
public class PlayChangelingOverlay extends Overlay implements InteractionOverlay {
public PlayChangelingOverlay(@NotNull GameScreen gameScreen, long timeout) {
super(gameScreen, timeout);
}
@Override
public Actor createContent() {
var root = new VerticalGroup().columnCenter().space(10);
var prompt = new Label(messages.get("game.overlay.play_changeling.prompt"), skin);
var cardGroup = new HorizontalGroup().space(20);
var wizard = new CardActor(Card.CHANGELING_WIZARD, atlas);
var jester = new CardActor(Card.CHANGELING_JESTER, atlas);
cardGroup.addActor(wizard);
cardGroup.addActor(jester);
cardGroup.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
if (isClosing()) return;
var target = event.getTarget();
if (target == wizard) {
screen.onCardClicked(Card.CHANGELING_WIZARD);
} else if (target == jester) {
screen.onCardClicked(Card.CHANGELING_JESTER);
}
}
});
root.addActor(prompt);
root.addActor(cardGroup);
var cancel = new TextButton(messages.get("game.overlay.play_changeling.cancel"), skin, "simple");
cancel.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
close();
}
});
root.addActor(cancel);
return root;
}
}

View File

@ -0,0 +1,81 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
import java.util.EnumMap;
public class PlayColoredCardOverlay extends Overlay implements InteractionOverlay {
private final EnumMap<Card.Suit, CardActor> actors = new EnumMap<>(Card.Suit.class);
private final EnumMap<Card.Suit, Card> cards = new EnumMap<>(Card.Suit.class);
private final Card card;
public PlayColoredCardOverlay(@NotNull GameScreen gameScreen, long timeout, @NotNull Card card,
@NotNull Card red, @NotNull Card green, @NotNull Card blue, @NotNull Card yellow)
{
super(gameScreen, timeout);
this.card = card;
this.cards.put(Card.Suit.RED, red);
this.cards.put(Card.Suit.GREEN, green);
this.cards.put(Card.Suit.BLUE, blue);
this.cards.put(Card.Suit.YELLOW, yellow);
}
@Override
public Actor createContent() {
var root = new VerticalGroup().columnCenter().space(10);
var prompt = new Label(messages.get("game.overlay.play_colored_card.prompt"), skin);
var cardGroup = new HorizontalGroup().space(20);
var card = new CardActor(this.card, atlas);
root.addActorAt(0, card);
root.padTop(- CardActor.PREF_HEIGHT);
actors.put(Card.Suit.RED, new CardActor(Card.Suit.RED, atlas));
actors.put(Card.Suit.GREEN, new CardActor(Card.Suit.GREEN, atlas));
actors.put(Card.Suit.BLUE, new CardActor(Card.Suit.BLUE, atlas));
actors.put(Card.Suit.YELLOW, new CardActor(Card.Suit.YELLOW, atlas));
actors.values().forEach(cardGroup::addActor);
cardGroup.addListener(new ClickListener() {
@Override
public void clicked(InputEvent event, float x, float y) {
if (isClosing()) return;
var target = event.getTarget();
for (Card.Suit suit : Card.Suit.values()) {
if (actors.get(suit) == target) {
screen.onCardClicked(cards.get(suit));
break;
}
}
}
});
root.addActor(prompt);
root.addActor(cardGroup);
var cancel = new TextButton(messages.get("game.overlay.play_colored_card.cancel"), skin, "simple");
cancel.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
close();
}
});
root.addActor(cancel);
return root;
}
}

View File

@ -0,0 +1,89 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.actors.PadOfTruth;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.changeParent;
import static eu.jonahbauer.wizard.client.libgdx.screens.GameScreen.PAD_OF_TRUTH_POSITION;
import static eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings.OVERLAY_SHARED_ELEMENT;
public class ScoreOverlay extends Overlay {
private final boolean finalScores;
private final PadOfTruth padOfTruth;
private TextButton back;
public ScoreOverlay(@NotNull GameScreen gameScreen, boolean finalScores) {
super(gameScreen, Long.MAX_VALUE);
this.finalScores = finalScores;
this.padOfTruth = Objects.requireNonNull(gameScreen.getPadOfTruth());
}
@Override
protected Actor createContent() {
var group = new Group();
group.addActor(padOfTruth);
if (finalScores) {
back = new TextButton(messages.get("game.overlay.scores.return_to_session"), skin);
back.setPosition(
(WizardGame.WIDTH - back.getWidth()) / 2,
(WizardGame.HEIGHT - padOfTruth.getHeight()) / 2 + 30 - back.getHeight()
);
back.setVisible(false);
back.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
screen.showSessionScreen();
}
});
group.addActor(back);
}
return group;
}
@Override
protected void show(Group parent) {
var root = getRoot().fill();
super.show(parent);
if (finalScores) {
root.addAction(sequence(
run(() -> padOfTruth.setEnabled(false)),
parallel(
targeting(padOfTruth, scaleTo(1, 1, OVERLAY_SHARED_ELEMENT)),
targeting(padOfTruth, moveTo((WizardGame.WIDTH - padOfTruth.getWidth()) / 2,(WizardGame.HEIGHT - padOfTruth.getHeight()) / 2 + 50, OVERLAY_SHARED_ELEMENT))
),
delay(AnimationTimings.OVERLAY_HOLD),
targeting(back, visible(true))
));
} else {
root.addAction(sequence(
run(() -> padOfTruth.setEnabled(false)),
parallel(
targeting(padOfTruth, scaleTo(1, 1, OVERLAY_SHARED_ELEMENT)),
targeting(padOfTruth, moveTo((WizardGame.WIDTH - padOfTruth.getWidth()) / 2,(WizardGame.HEIGHT - padOfTruth.getHeight()) / 2, OVERLAY_SHARED_ELEMENT))
),
delay(AnimationTimings.OVERLAY_HOLD),
parallel(
targeting(padOfTruth, scaleTo(PadOfTruth.COLLAPSED_SCALE, PadOfTruth.COLLAPSED_SCALE, OVERLAY_SHARED_ELEMENT)),
targeting(padOfTruth, moveTo(PAD_OF_TRUTH_POSITION.x, PAD_OF_TRUTH_POSITION.y, OVERLAY_SHARED_ELEMENT) )
),
targeting(padOfTruth, changeParent(screen.getContentRoot())),
run(() -> padOfTruth.setEnabled(true)),
run(this::close)
));
}
}
}

View File

@ -0,0 +1,43 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import org.jetbrains.annotations.NotNull;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
public class StartRoundOverlay extends Overlay {
private final int round;
public StartRoundOverlay(@NotNull GameScreen gameScreen, int round) {
super(gameScreen, Long.MAX_VALUE);
this.round = round;
}
@Override
public Actor createContent() {
var root = new VerticalGroup().columnCenter().space(10f);
var label = new Label(messages.format("game.overlay.round.title", round + 1), skin, "enchanted");
label.setFontScale(1.5f);
root.addActor(label);
return root;
}
@Override
public void show(Group parent) {
super.show(parent);
var root = getRoot();
root.addAction(sequence(
delay(AnimationTimings.OVERLAY_HOLD),
run(this::close)
));
}
}

View File

@ -0,0 +1,162 @@
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.actions.MyActions;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.client.libgdx.util.CardUtil;
import eu.jonahbauer.wizard.common.model.Card;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
import static eu.jonahbauer.wizard.client.libgdx.actions.MyActions.*;
import static eu.jonahbauer.wizard.client.libgdx.screens.GameScreen.*;
import static eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings.OVERLAY_SHARED_ELEMENT;
public class TrumpOverlay extends Overlay {
private final @Nullable String player;
private final @Nullable Card card;
private final @Nullable Card.Suit suit;
private final @NotNull CardActor trumpCardActor;
private final @NotNull CardActor trumpSuitActor;
private boolean animateCard = true;
private boolean animateSuit = true;
public TrumpOverlay(@NotNull GameScreen gameScreen, @Nullable String player, @Nullable Card card, @Nullable Card.Suit suit) {
super(gameScreen, Long.MAX_VALUE);
this.player = player;
this.card = card;
this.suit = suit;
this.trumpCardActor = Objects.requireNonNull(gameScreen.getTrumpCardActor());
this.trumpSuitActor = Objects.requireNonNull(gameScreen.getTrumpSuitActor());
}
@Override
public Actor createContent() {
var root = new VerticalGroup().columnCenter().space(10f);
String text;
if (player == null) {
text = messages.get(suit != null ? switch (suit) {
case YELLOW -> "game.overlay.trump.yellow";
case GREEN -> "game.overlay.trump.green";
case BLUE -> "game.overlay.trump.blue";
case RED -> "game.overlay.trump.red";
default -> "game.overlay.trump.none";
} : "game.overlay.trump.unknown");
} else {
text = messages.format(suit != null ? switch (suit) {
case YELLOW -> "game.overlay.trump.yellow.player";
case GREEN -> "game.overlay.trump.green.player";
case BLUE -> "game.overlay.trump.blue.player";
case RED -> "game.overlay.trump.red.player";
default -> "game.overlay.trump.none.player";
} : "game.overlay.trump.unknown.player", player);
}
var label = new Label(text, skin);
label.getStyle().font.getData().markupEnabled = true;
label.setFontScale(1.5f);
root.addActor(label);
var cardGroup = new HorizontalGroup().space(20);
root.addActor(cardGroup);
if (trumpCardActor.hasParent() && trumpCardActor.getCard() == card && suit != null) {
// if card actor is already correct then dont change it
animateCard = false;
} else {
cardGroup.addActor(trumpCardActor);
trumpCardActor.setCard(card != null ? card : Card.HIDDEN);
}
animateSuit = suit != null && suit != CardUtil.getDefaultTrumpSuit(card);
if (animateSuit) {
trumpSuitActor.setRotation(0);
cardGroup.addActor(trumpSuitActor);
trumpSuitActor.setCard(suit);
} else {
trumpSuitActor.remove();
}
return root;
}
@Override
public void show(Group parent) {
super.show(parent);
boolean cardVisible = trumpCardActor.hasParent();
var cardAnimation = sequence();
// remove from parent
var parallel = parallel();
if (animateSuit) {
parallel.addAction(targeting(trumpSuitActor, removeActorSilently()));
}
if (animateCard) {
parallel.addAction(targeting(trumpCardActor, removeActorSilently()));
}
cardAnimation.addAction(parallel);
// change parent in correct order
parallel = parallel();
if (animateSuit) {
parallel.addAction(targeting(trumpSuitActor, changeParent(screen.getOverlayRoot())));
}
if (cardVisible) {
parallel.addAction(targeting(trumpCardActor, changeParent(screen.getOverlayRoot())));
}
cardAnimation.addAction(parallel);
// animate
parallel = parallel();
if (animateSuit) {
parallel.addAction(targeting(trumpSuitActor, rotateTo(TRUMP_SUIT_ROTATION, OVERLAY_SHARED_ELEMENT)));
parallel.addAction(
targeting(trumpSuitActor, moveTo(TRUMP_SUIT_POSITION.x, TRUMP_SUIT_POSITION.y, OVERLAY_SHARED_ELEMENT))
);
}
if (animateCard) {
parallel.addAction(targeting(trumpCardActor, moveTo(TRUMP_CARD_POSITION.x, TRUMP_CARD_POSITION.y, OVERLAY_SHARED_ELEMENT)));
}
cardAnimation.addAction(parallel);
// change parent in correct order
parallel = parallel();
if (animateSuit) {
parallel.addAction(targeting(trumpSuitActor, changeParent(screen.getContentRoot())));
}
if (cardVisible) {
parallel.addAction(targeting(trumpCardActor, changeParent(screen.getContentRoot())));
}
cardAnimation.addAction(parallel);
var root = getRoot();
root.addAction(sequence(
delay(AnimationTimings.OVERLAY_HOLD),
cardAnimation,
run(this::close)
));
}
@Override
protected void onClosing() {
var actions = getRoot().getActions();
if (actions.size > 0) MyActions.finish(actions.get(0));
onClosed();
}
}

View File

@ -0,0 +1,162 @@
package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import eu.jonahbauer.wizard.client.libgdx.GameAtlas;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.NoSuchElementException;
@Getter
@Setter
public class CardActor extends Actor {
public static final float ASPECT_RATIO = 400f / 260f;
public static final float PREF_WIDTH = 135;
public static final float PREF_HEIGHT = 208;
private static final Map<Card, String> ATLAS_PATHS;
static {
var paths = new EnumMap<Card, String>(Card.class);
for (Card card : Card.values()) {
paths.put(card, switch (card) {
case BLUE_1 -> GameAtlas.CARDS_BLUE_1;
case BLUE_2 -> GameAtlas.CARDS_BLUE_2;
case BLUE_3 -> GameAtlas.CARDS_BLUE_3;
case BLUE_4 -> GameAtlas.CARDS_BLUE_4;
case BLUE_5 -> GameAtlas.CARDS_BLUE_5;
case BLUE_6 -> GameAtlas.CARDS_BLUE_6;
case BLUE_7 -> GameAtlas.CARDS_BLUE_7;
case BLUE_8 -> GameAtlas.CARDS_BLUE_8;
case BLUE_9 -> GameAtlas.CARDS_BLUE_9;
case BLUE_10 -> GameAtlas.CARDS_BLUE_10;
case BLUE_11 -> GameAtlas.CARDS_BLUE_11;
case BLUE_12 -> GameAtlas.CARDS_BLUE_12;
case BLUE_13 -> GameAtlas.CARDS_BLUE_13;
case RED_1 -> GameAtlas.CARDS_RED_1;
case RED_2 -> GameAtlas.CARDS_RED_2;
case RED_3 -> GameAtlas.CARDS_RED_3;
case RED_4 -> GameAtlas.CARDS_RED_4;
case RED_5 -> GameAtlas.CARDS_RED_5;
case RED_6 -> GameAtlas.CARDS_RED_6;
case RED_7 -> GameAtlas.CARDS_RED_7;
case RED_8 -> GameAtlas.CARDS_RED_8;
case RED_9 -> GameAtlas.CARDS_RED_9;
case RED_10 -> GameAtlas.CARDS_RED_10;
case RED_11 -> GameAtlas.CARDS_RED_11;
case RED_12 -> GameAtlas.CARDS_RED_12;
case RED_13 -> GameAtlas.CARDS_RED_13;
case YELLOW_1 -> GameAtlas.CARDS_YELLOW_1;
case YELLOW_2 -> GameAtlas.CARDS_YELLOW_2;
case YELLOW_3 -> GameAtlas.CARDS_YELLOW_3;
case YELLOW_4 -> GameAtlas.CARDS_YELLOW_4;
case YELLOW_5 -> GameAtlas.CARDS_YELLOW_5;
case YELLOW_6 -> GameAtlas.CARDS_YELLOW_6;
case YELLOW_7 -> GameAtlas.CARDS_YELLOW_7;
case YELLOW_8 -> GameAtlas.CARDS_YELLOW_8;
case YELLOW_9 -> GameAtlas.CARDS_YELLOW_9;
case YELLOW_10 -> GameAtlas.CARDS_YELLOW_10;
case YELLOW_11 -> GameAtlas.CARDS_YELLOW_11;
case YELLOW_12 -> GameAtlas.CARDS_YELLOW_12;
case YELLOW_13 -> GameAtlas.CARDS_YELLOW_13;
case GREEN_1 -> GameAtlas.CARDS_GREEN_1;
case GREEN_2 -> GameAtlas.CARDS_GREEN_2;
case GREEN_3 -> GameAtlas.CARDS_GREEN_3;
case GREEN_4 -> GameAtlas.CARDS_GREEN_4;
case GREEN_5 -> GameAtlas.CARDS_GREEN_5;
case GREEN_6 -> GameAtlas.CARDS_GREEN_6;
case GREEN_7 -> GameAtlas.CARDS_GREEN_7;
case GREEN_8 -> GameAtlas.CARDS_GREEN_8;
case GREEN_9 -> GameAtlas.CARDS_GREEN_9;
case GREEN_10 -> GameAtlas.CARDS_GREEN_10;
case GREEN_11 -> GameAtlas.CARDS_GREEN_11;
case GREEN_12 -> GameAtlas.CARDS_GREEN_12;
case GREEN_13 -> GameAtlas.CARDS_GREEN_13;
case JUGGLER -> GameAtlas.CARDS_JUGGLER;
case JUGGLER_BLUE -> GameAtlas.CARDS_JUGGLER_BLUE;
case JUGGLER_RED -> GameAtlas.CARDS_JUGGLER_RED;
case JUGGLER_GREEN -> GameAtlas.CARDS_JUGGLER_GREEN;
case JUGGLER_YELLOW -> GameAtlas.CARDS_JUGGLER_YELLOW;
case CLOUD -> GameAtlas.CARDS_CLOUD;
case CLOUD_GREEN -> GameAtlas.CARDS_CLOUD_GREEN;
case CLOUD_RED -> GameAtlas.CARDS_CLOUD_RED;
case CLOUD_YELLOW -> GameAtlas.CARDS_CLOUD_YELLOW;
case CLOUD_BLUE -> GameAtlas.CARDS_CLOUD_BLUE;
case BOMB -> GameAtlas.CARDS_BOMB;
case FAIRY -> GameAtlas.CARDS_FAIRY;
case DRAGON -> GameAtlas.CARDS_DRAGON;
case CHANGELING -> GameAtlas.CARDS_CHANGELING;
case CHANGELING_WIZARD -> GameAtlas.CARDS_CHANGELING_WIZARD;
case CHANGELING_JESTER -> GameAtlas.CARDS_CHANGELING_JESTER;
case BLUE_JESTER -> GameAtlas.CARDS_BLUE_JESTER;
case RED_JESTER -> GameAtlas.CARDS_RED_JESTER;
case GREEN_JESTER -> GameAtlas.CARDS_GREEN_JESTER;
case YELLOW_JESTER -> GameAtlas.CARDS_YELLOW_JESTER;
case RED_WIZARD -> GameAtlas.CARDS_RED_WIZARD;
case GREEN_WIZARD -> GameAtlas.CARDS_GREEN_WIZARD;
case YELLOW_WIZARD -> GameAtlas.CARDS_YELLOW_WIZARD;
case BLUE_WIZARD -> GameAtlas.CARDS_BLUE_WIZARD;
case WEREWOLF -> GameAtlas.CARDS_WEREWOLF;
default -> GameAtlas.CARDS_BACKGROUND;
});
}
ATLAS_PATHS = Collections.unmodifiableMap(paths);
}
private final TextureAtlas atlas;
private Card card;
private TextureRegion background;
private CardActor(TextureAtlas atlas) {
this.atlas = atlas;
setWidth(PREF_WIDTH);
setHeight(PREF_HEIGHT);
setOrigin(PREF_WIDTH / 2, PREF_HEIGHT / 2);
}
public CardActor(@NotNull Card card, @NotNull TextureAtlas atlas) {
this(atlas);
setCard(card);
}
public CardActor(@NotNull Card.Suit suit, @NotNull TextureAtlas atlas) {
this(atlas);
setCard(suit);
}
public void setCard(@NotNull Card card) {
this.card = card;
this.background = atlas.findRegion(ATLAS_PATHS.get(card));
if (this.background == null) throw new NoSuchElementException("Could not find texture for card " + card + ".");
}
public void setCard(@NotNull Card.Suit suit) {
this.card = null;
this.background = atlas.findRegion(switch (suit) {
case NONE -> GameAtlas.CARDS_BACKGROUND;
case GREEN -> GameAtlas.CARDS_GREEN;
case YELLOW -> GameAtlas.CARDS_YELLOW;
case BLUE -> GameAtlas.CARDS_BLUE;
case RED -> GameAtlas.CARDS_RED;
});
if (this.background == null) throw new NoSuchElementException("Could not find texture for suit " + suit + ".");
}
@Override
public void draw(Batch batch, float parentAlpha) {
var color = getColor();
batch.setColor(color.r, color.g, color.b, color.a * parentAlpha);
float height = getWidth() * ASPECT_RATIO;
batch.draw(background, getX(), getY() + getHeight() - height, getOriginX(), getOriginY(), getWidth(), height, getScaleX(), getScaleY(), getRotation());
}
}

View File

@ -0,0 +1,173 @@
package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.*;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Data;
import java.util.*;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;
public class CardStack extends Group {
private static final float EXPANDED_ROTATION_DEVIATION = 10;
private static final float COLLAPSED_ROTATION_DEVIATION = 60;
private static final float COLLAPSED_POSITION_DEVIATION = 15;
private final Random random = new Random();
private final List<Entry> cards = new ArrayList<>();
private final Actor hover = new Actor();
private boolean expanded;
public CardStack() {
this.addActor(hover);
this.addListener(new InputListener() {
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (pointer == -1 && event.getTarget() == hover && expanded) {
expanded = false;
cards.forEach(Entry::collapse);
}
}
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (pointer == -1 && event.getTarget() == hover && !expanded) {
expanded = true;
cards.forEach(Entry::expand);
}
}
});
}
public void add(GameScreen.Seat seat, CardActor card) {
var entry = new Entry(
card,
seat,
(float) random.nextGaussian((WizardGame.WIDTH - card.getWidth()) / 2, COLLAPSED_POSITION_DEVIATION),
(float) random.nextGaussian((WizardGame.HEIGHT - card.getHeight()) / 2, COLLAPSED_POSITION_DEVIATION),
(float) random.nextGaussian(0, COLLAPSED_ROTATION_DEVIATION),
(float) random.nextGaussian(0, EXPANDED_ROTATION_DEVIATION)
);
super.addActor(card);
if (expanded) entry.expand();
else entry.collapse();
cards.add(entry);
}
public void add(GameScreen.Seat seat, Card card, TextureAtlas atlas) {
var actor = new CardActor(card, atlas);
var x = (float) random.nextGaussian((WizardGame.WIDTH - actor.getWidth()) / 2, COLLAPSED_POSITION_DEVIATION);
var y = (float) random.nextGaussian((WizardGame.HEIGHT - actor.getHeight()) / 2, COLLAPSED_POSITION_DEVIATION);
var collapsedRotation = (float) random.nextGaussian(0, COLLAPSED_ROTATION_DEVIATION);
var expandedRotation = (float) random.nextGaussian(0, EXPANDED_ROTATION_DEVIATION);
if (expanded) {
actor.setPosition(seat.getFrontX(), seat.getFrontY());
actor.setRotation(expandedRotation);
} else {
actor.setPosition(x, y);
actor.setRotation(collapsedRotation);
}
var entry = new Entry(actor, seat, x, y, collapsedRotation, expandedRotation);
super.addActor(actor);
cards.add(entry);
}
@Override
protected void childrenChanged() {
hover.toFront();
}
@Override
public void clearChildren(boolean unfocus) {
super.clearChildren(unfocus);
cards.clear();
addActor(hover);
}
public List<CardActor> removeAll() {
var out = cards.stream().map(Entry::getActor).toList();
clearChildren(true);
return out;
}
@Override
public Actor removeActorAt(int index, boolean unfocus) {
var actor = super.removeActorAt(index, unfocus);
cards.remove(index);
return actor;
}
public void setHoverBounds(float x, float y, float width, float height) {
hover.setBounds(x, y, width, height);
}
@Override
@Deprecated
public void addActor(Actor actor) {
super.addActor(actor);
}
@Override
@Deprecated
public void addActorAfter(Actor actorAfter, Actor actor) {
super.addActorAfter(actorAfter, actor);
}
@Override
@Deprecated
public void addActorAt(int index, Actor actor) {
super.addActorAt(index, actor);
}
@Override
@Deprecated
public void addActorBefore(Actor actorBefore, Actor actor) {
super.addActorBefore(actorBefore, actor);
}
@Data
private static class Entry {
private final CardActor actor;
private final GameScreen.Seat seat;
private final float x;
private final float y;
private final float rotation;
private final float expandedRotation;
private Action action;
public void expand() {
if (action != null) actor.removeAction(action);
action = parallel(
moveTo(
seat.getFrontX() - actor.getWidth() / 2,
seat.getFrontY() - actor.getHeight() / 2,
AnimationTimings.STACK_EXPAND
),
rotateTo(expandedRotation, AnimationTimings.STACK_EXPAND)
);
actor.addAction(action);
}
public void collapse() {
if (action != null) actor.removeAction(action);
action = parallel(
moveTo(x, y, AnimationTimings.STACK_COLLAPSE),
rotateTo(rotation, AnimationTimings.STACK_COLLAPSE)
);
actor.addAction(action);
}
}
}

View File

@ -0,0 +1,249 @@
package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
import com.badlogic.gdx.utils.Pools;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import eu.jonahbauer.wizard.client.libgdx.util.Pair;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.moveTo;
public class CardsGroup extends WidgetGroup {
private static final float TARGET_SPACING = -50f;
@Getter
private final float prefWidth = 0;
@Getter
private final float prefHeight = CardActor.PREF_HEIGHT;
private final TextureAtlas atlas;
private float[] cardX;
private float spacing;
private float cardWidth;
private boolean animate;
private CardActor target;
private boolean dragging;
private final float touchSlop = 5;
private CardActor dragTarget;
private float dragStartX;
@Setter
@Getter
private Card selected;
private Consumer<CardActor> onClickListener;
public CardsGroup(List<Card> cards, TextureAtlas atlas) {
this.atlas = atlas;
update(cards);
setFillParent(true);
this.addListener(new InputListener() {
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
if (pointer != 0) return false;
if (button != Input.Buttons.LEFT) return false;
if (event.getTarget() instanceof CardActor card) {
dragTarget = card;
dragStartX = x;
return true;
}
return false;
}
@Override
public void touchDragged(InputEvent event, float x, float y, int pointer) {
if (pointer != 0) return;
if (!dragging && Math.abs(x - dragStartX) > touchSlop) {
dragging = true;
} else if (dragging) {
int index = Arrays.binarySearch(cardX, x - (cardWidth + spacing));
if (index < 0) {
index = -(index + 1);
if (index >= getChildren().size) return;
}
if (getChild(index) != dragTarget) {
float oldX = dragTarget.getX();
getChildren().removeValue(dragTarget, true);
getChildren().insert(index, dragTarget);
invalidate();
dragTarget.setHeight(1.3f * getHeight());
dragStartX += (dragTarget.getX() - oldX);
}
}
}
@Override
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
if (pointer != 0) return;
if (button != Input.Buttons.LEFT) return;
if (!dragging && dragTarget != null) {
if (onClickListener != null) {
onClickListener.accept(dragTarget);
}
}
dragging = false;
dragTarget = null;
}
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (pointer != -1) return;
if (event.getTarget() instanceof CardActor card) {
target = card;
}
}
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (pointer != -1) return;
if (event.getTarget() == target) {
target = null;
}
}
});
}
@Override
public void act(float delta) {
super.act(delta);
float offset = 0.3f * getHeight();
float speed = 10 * offset;
for (var child : getChildren()) {
if (child instanceof CardActor card) {
float height = card.getHeight();
if ((child == dragTarget || dragTarget == null && child == target) || card.getCard() == selected) {
height += speed * delta;
} else {
height -= speed * delta;
}
height = MathUtils.clamp(height, getHeight(), getHeight() + offset);
card.setHeight(height);
}
}
}
@Override
public void layout() {
var children = getChildren();
var count = children.size;
float height = getHeight();
float width = getWidth();
if (cardX == null || cardX.length != count) {
cardX = new float[count];
}
cardWidth = height / CardActor.ASPECT_RATIO;
spacing = width - count * cardWidth;
spacing /= count - 1;
spacing = Math.min(spacing, TARGET_SPACING);
float x = Math.max(0, (width - count * cardWidth - (count - 1) * TARGET_SPACING) / 2);
float y = 0;
for (int i = 0; i < children.size; i++) {
var child = children.get(i);
// position
if (animate) {
child.addAction(moveTo(x, y, AnimationTimings.HAND_LAYOUT));
} else {
child.setPosition(x, y);
}
// size
child.setWidth(cardWidth);
if (child != dragTarget && child != target) {
child.setHeight(height);
}
cardX[i] = x;
x += cardWidth + spacing;
}
animate = false;
}
public @NotNull Pair<List<CardActor>, List<CardActor>> update(@NotNull List<Card> cards) {
var added = new ArrayList<>(cards);
var removed = new ArrayList<Card>();
for (var child : getChildren()) {
if (child instanceof CardActor actor) {
var card = actor.getCard();
if (!added.remove(card)) {
removed.add(card);
}
}
}
var removedActors = removed.stream().map(this::remove).toList();
var addedActors = added.stream().sorted().map(card -> new CardActor(card, atlas)).toList();
addedActors.forEach(this::addActor);
layout();
return Pair.of(removedActors, addedActors);
}
public @Nullable CardActor remove(Card card) {
var actor = find(card);
if (actor == null) return null;
// adjust actor
actor.setY(getY() + actor.getHeight() - getHeight());
var pos = localToStageCoordinates(Pools.get(Vector2.class).obtain().set(actor.getX(), actor.getY()));
actor.setHeight(getHeight());
actor.setPosition(pos.x, pos.y);
if (target == actor) {
target = null;
}
// remove actor
animate = true;
actor.clearActions();
actor.remove();
return actor;
}
public @Nullable CardActor find(Card card) {
var children = getChildren();
for (int i = 0; i < children.size; i++) {
if (children.get(i) instanceof CardActor cardActor && cardActor.getCard() == card) {
return cardActor;
}
}
return null;
}
@Override
public float getMinHeight() {
return 0;
}
public void setOnClickListener(Consumer<CardActor> onClickListener) {
this.onClickListener = onClickListener;
}
}

View File

@ -0,0 +1,126 @@
package eu.jonahbauer.wizard.client.libgdx.actors;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.actions.ScaleToAction;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings;
import lombok.Setter;
import org.jetbrains.annotations.Range;
public class PadOfTruth extends Table {
public static final float EXTENDED_WIDTH = 636;
public static final float EXTENDED_HEIGHT = 824;
public static final float COLLAPSED_SCALE = CardActor.PREF_HEIGHT / EXTENDED_HEIGHT;
private final Label[] names = new Label[6];
private final Label[][] predictions = new Label[20][];
private final Label[][] scores = new Label[20][];
@Setter
private boolean enabled = true;
public PadOfTruth(Skin skin, Drawable background) {
super(skin);
setTouchable(Touchable.enabled);
setBackground(background);
setWidth(EXTENDED_WIDTH);
setHeight(EXTENDED_HEIGHT);
setTransform(true);
setScale(COLLAPSED_SCALE);
addListener(new InputListener() {
private ScaleToAction action;
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (!enabled) return;
if (fromActor != null && isAscendantOf(fromActor)) return;
if (action != null) removeAction(action);
action = Actions.scaleTo(1, 1, AnimationTimings.PAD_OF_TRUTH_EXPAND);
addAction(action);
}
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (!enabled) return;
if (toActor != null && isAscendantOf(toActor)) return;
if (action != null) removeAction(action);
action = Actions.scaleTo(COLLAPSED_SCALE, COLLAPSED_SCALE, AnimationTimings.PAD_OF_TRUTH_COLLAPSE);
addAction(action);
}
});
setRound(false);
pad(20, 45, 25, 25);
align(Align.topLeft);
clip();
for (int i = 0; i < 6; i++) {
columnDefaults(2 * i).width(57f).center().pad(2, 4, 2, 4);
columnDefaults(2 * i + 1).width(22f).center().pad(2, 4, 2, 4);
}
for (int player = 0; player < 6; player++) {
var cell = add("", "handwritten").height(46f).width(87f).colspan(2);
names[player] = cell.getActor();
names[player].setEllipsis(true);
}
row();
for (int round = 0; round < 20; round++) {
int players = MathUtils.clamp(60 / (round + 1), 3, 6);
predictions[round] = new Label[players];
scores[round] = new Label[players];
for (int player = 0; player < players; player++) {
scores[round][player] = add("", "handwritten").height(32.5f).center().getActor();
predictions[round][player] = add("", "handwritten").height(32.5f).center().getActor();
scores[round][player].setAlignment(Align.center);
predictions[round][player].setAlignment(Align.center);
}
row();
}
}
public void setName(int player, String name) {
names[player].setText(name);
}
public void setScore(int player, @Range(from = 0, to = 19) int round, int score) {
scores[round][player].setText(String.valueOf(score));
}
public void setPrediction(int player, @Range(from = 0, to = 19) int round, int prediction) {
predictions[round][player].setText(String.valueOf(prediction));
}
public void checkPosition(int player, int round) {
if (round < 0 || round >= predictions.length || round >= scores.length) throw new ArrayIndexOutOfBoundsException(round);
if (player < 0 || player >= predictions[round].length || player >= scores[round].length) throw new ArrayIndexOutOfBoundsException(player);
}
public void clearValues() {
for (var row : predictions) {
for (var label : row) {
label.setText(null);
}
}
for (var row : scores) {
for (var label : row) {
label.setText(null);
}
}
}
}

View File

@ -0,0 +1,36 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
public class AutoFocusListener extends InputListener {
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
var target = event.getTarget();
while (target != null && !(target instanceof ScrollPane)) {
target = target.getParent();
}
if (target instanceof ScrollPane pane) {
event.getStage().setScrollFocus(pane);
} else {
event.getStage().setScrollFocus(null);
}
}
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
var target = event.getTarget();
while (target != null && !(target instanceof ScrollPane)) {
target = target.getParent();
}
if (target == null || toActor == null || !toActor.isDescendantOf(target)) {
event.getStage().setScrollFocus(null);
}
}
}

View File

@ -0,0 +1,17 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
public class ButtonKeyListener extends InputListener {
@Override
public boolean keyTyped(InputEvent event, char character) {
if ((character == '\n' || character == ' ') && event.getTarget() instanceof Button button) {
button.toggle();
return true;
}
return false;
}
}

View File

@ -0,0 +1,47 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.utils.UIUtils;
import java.util.List;
public class KeyboardFocusManager extends InputListener {
private final List<Actor> focusOrder;
public KeyboardFocusManager(Actor...focusOrder) {
this.focusOrder = List.of(focusOrder);
}
@Override
public boolean keyTyped(InputEvent event, char character) {
var stage = event.getStage();
if (character == '\t') {
var currentFocus = stage.getKeyboardFocus();
var index = currentFocus == null ? -1 : focusOrder.indexOf(currentFocus);
var count = focusOrder.size();
if (count == 0) return true;
Actor nextFocus;
if (index == -1) {
nextFocus = focusOrder.get(UIUtils.shift() ? count - 1 : 0);
} else {
var direction = UIUtils.shift() ? -1 : 1;
nextFocus = focusOrder.get(((index + direction) % count + count) % count);
}
if (nextFocus instanceof TextField textField) {
textField.selectAll();
}
stage.setKeyboardFocus(nextFocus);
event.stop();
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,39 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
public class ResetErrorListener extends ChangeListener {
private final Skin skin;
private final Actor target;
private final String style;
public ResetErrorListener(Skin skin) {
this(skin, null);
}
public ResetErrorListener(Skin skin, Actor target) {
this(skin, target, "default");
}
public ResetErrorListener(Skin skin, Actor target, String style) {
this.skin = skin;
this.target = target;
this.style = style;
}
@Override
public void changed(ChangeEvent event, Actor actor) {
var target = this.target != null ? this.target : event.getTarget();
if (target instanceof TextField textField) {
textField.setStyle(skin.get(style, TextField.TextFieldStyle.class));
} else if (target instanceof SelectBox<?> box) {
box.setStyle(skin.get(style, SelectBox.SelectBoxStyle.class));
} else if (target instanceof List<?> list) {
list.setStyle(skin.get(style, List.ListStyle.class));
} else if (target instanceof ScrollPane scrollPane) {
scrollPane.setStyle(skin.get(style, ScrollPane.ScrollPaneStyle.class));
}
}
}

View File

@ -0,0 +1,44 @@
package eu.jonahbauer.wizard.client.libgdx.listeners;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox;
public class SelectBoxListener extends InputListener {
private final SelectBox<?> selectBox;
public SelectBoxListener(SelectBox<?> selectBox) {
this.selectBox = selectBox;
}
@Override
public boolean keyDown(InputEvent event, int keycode) {
var size = selectBox.getItems().size;
if (size == 0) return false;
switch (keycode) {
case Input.Keys.UP -> {
var index = selectBox.getSelectedIndex();
if (index == -1) {
selectBox.setSelectedIndex(size - 1);
} else {
selectBox.setSelectedIndex(MathUtils.clamp(index - 1, 0, size - 1));
}
return true;
}
case Input.Keys.DOWN -> {
var index = selectBox.getSelectedIndex();
if (index == -1) {
selectBox.setSelectedIndex(0);
} else {
selectBox.setSelectedIndex(MathUtils.clamp(index + 1, 0, size - 1));
}
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,80 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.TextField;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.state.Menu;
import java.net.URI;
import java.net.URISyntaxException;
public class ConnectScreen extends MenuScreen {
private TextButton buttonBack;
private TextButton buttonConnect;
private TextField uriField;
public ConnectScreen(WizardGame game) {
super(game);
}
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(Menu.class, Menu::showMenuScreen);
sfxClick();
} else if (actor == buttonConnect) {
try {
var uriString = ConnectScreen.this.uriField.getText();
var uri = new URI(uriString);
game.storage.uri = uriString;
game.getClient().execute(Menu.class, (s, c) -> s.connect(c, uri));
} catch (URISyntaxException e) {
uriField.setStyle(getTextFieldErrorStyle());
}
sfxClick();
}
}
};
@Override
public void show() {
super.show();
buttonBack = new TextButton(messages.get("menu.connect.back"), skin);
buttonBack.addListener(listener);
getButtonGroup().addActor(buttonBack);
buttonConnect = new TextButton(messages.get("menu.connect.connect"), skin);
buttonConnect.addListener(listener);
getButtonGroup().addActor(buttonConnect);
var label = new Label(messages.get("menu.connect.address.label"), skin);
label.setSize(0.4f * WizardGame.WIDTH, 64);
label.setAlignment(Align.center);
label.setPosition(0.5f * (WizardGame.WIDTH - label.getWidth()), 0.55f * (WizardGame.HEIGHT - label.getHeight()));
// TODO sensible default value
uriField = new TextField(game.storage.uri, skin);
uriField.setMessageText(messages.get("menu.connect.uri.hint"));
uriField.setSize(0.4f * WizardGame.WIDTH, 64);
uriField.setPosition(0.5f * (WizardGame.WIDTH - uriField.getWidth()), 0.45f * (WizardGame.HEIGHT - uriField.getHeight()));
uriField.addListener(new ResetErrorListener(skin));
stage.addActor(uriField);
stage.addActor(label);
stage.addCaptureListener(new KeyboardFocusManager(uriField, buttonBack, buttonConnect));
buttonBack.setName("button_back");
buttonConnect.setName("button_connect");
uriField.setName("uri");
}
}

View File

@ -0,0 +1,184 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Array;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.listeners.SelectBoxListener;
import eu.jonahbauer.wizard.client.libgdx.state.Lobby;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.extern.log4j.Log4j2;
@Log4j2
public class CreateGameScreen extends MenuScreen {
private static final int MAX_SESSION_NAME_LENGTH = 20;
private TextButton buttonBack;
private TextButton buttonContinue;
private TextField sessionName;
private TextField playerName;
private TextField timeOut;
private SelectBox<String> configurations;
private String oldPlayerName;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(Lobby.class, Lobby::showListScreen);
sfxClick();
} else if (actor == buttonContinue) {
create();
sfxClick();
}
}
};
public CreateGameScreen(WizardGame game) {
super(game);
oldPlayerName = game.storage.playerName;
}
@Override
public void show() {
super.show();
buttonBack = new TextButton(messages.get("menu.create_game.back"), skin);
buttonBack.addListener(listener);
getButtonGroup().addActor(buttonBack);
buttonContinue = new TextButton(messages.get("menu.create_game.create"), skin);
buttonContinue.addListener(listener);
getButtonGroup().addActor(buttonContinue);
var errorListener = new ResetErrorListener(skin);
sessionName = new TextField("", skin);
sessionName.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.5f);
sessionName.setSize(0.4f * WizardGame.WIDTH, 64);
sessionName.setMaxLength(MAX_SESSION_NAME_LENGTH);
sessionName.addListener(errorListener);
sessionName.setProgrammaticChangeEvents(true);
playerName = new TextField(oldPlayerName, skin);
playerName.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.45f);
playerName.setSize(0.4f * WizardGame.WIDTH, 64);
playerName.addListener(errorListener);
var playerNameListener = new ChangeListener() {
private final String format = messages.get("menu.create_game.session_name.default");
@Override
public void changed(ChangeEvent event, Actor actor) {
var player = playerName.getText();
var session = sessionName.getText();
var previousSuggestion = format.formatted(oldPlayerName);
boolean shouldApplySuggestion = session.isEmpty()
|| previousSuggestion.startsWith(session) && (session.length() == MAX_SESSION_NAME_LENGTH || session.length() == previousSuggestion.length());
if (shouldApplySuggestion) {
if (player.isEmpty()) {
sessionName.setText("");
} else {
sessionName.setText(format.formatted(player));
}
}
game.storage.playerName = player;
oldPlayerName = player;
}
};
playerName.addListener(playerNameListener);
playerNameListener.changed(null, null);
timeOut = new TextField("", skin);
timeOut.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.4f);
timeOut.setSize(0.4f * WizardGame.WIDTH, 64);
timeOut.setTextFieldFilter(new TextField.TextFieldFilter.DigitsOnlyFilter());
timeOut.addListener(errorListener);
configurations = new SelectBox<>(skin);
configurations.setSize(400, 64);
configurations.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f);
configurations.addListener(errorListener);
configurations.addListener(new SelectBoxListener(configurations));
Array<String> values = new Array<>();
for (Configuration value : Configuration.values()) {
values.add(value.toString());
}
configurations.setItems(values);
var contentTable = new Table(skin).center().left();
contentTable.columnDefaults(0).growX().width(0.4f * WizardGame.WIDTH - 20);
contentTable.setSize(0.4f * WizardGame.WIDTH - 20, 400);
contentTable.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f);
contentTable.add(messages.get("menu.create_game.player_name.label")).row();
contentTable.add(playerName).row();
contentTable.add(messages.get("menu.create_game.session_name.label")).row();
contentTable.add(sessionName).row();
contentTable.add(messages.get("menu.create_game.session_timeout.label")).row();
contentTable.add(timeOut).row();
contentTable.add(messages.get("menu.create_game.session_configuration.label")).row();
contentTable.add(configurations).row();
stage.addActor(contentTable);
stage.addCaptureListener(new KeyboardFocusManager(playerName, sessionName, timeOut, configurations, buttonBack, buttonContinue));
buttonBack.setName("button_back");
buttonContinue.setName("button_continue");
sessionName.setName("session_name");
playerName.setName("player_name");
timeOut.setName("timeout");
configurations.setName("configurations");
}
private void create() {
boolean error = false;
String sessionName = this.sessionName.getText();
if (sessionName.isBlank()) {
log.warn("Please choose a session name.");
this.sessionName.setStyle(getTextFieldErrorStyle());
error = true;
}
String playerName = this.playerName.getText();
if (playerName.isBlank()) {
log.warn("Please choose a name.");
this.playerName.setStyle(getTextFieldErrorStyle());
error = true;
}
long timeout = 0;
try {
timeout = Long.parseLong(this.timeOut.getText());
} catch (NumberFormatException e) {
log.warn("Please choose a valid timeout.");
this.timeOut.setStyle(getTextFieldErrorStyle());
error = true;
}
Configuration config = null;
try {
int selected = configurations.getSelectedIndex();
config = Configuration.values()[selected];
} catch (ArrayIndexOutOfBoundsException e) {
log.warn("Please select a valid configuration.");
this.configurations.setStyle(getSelectBoxErrorStyle());
error = true;
}
var fConfig = config;
var fTimeout = timeout;
if (!error) {
var client = game.getClient();
client.execute(Lobby.class, (s, c) -> s.createSession(c, sessionName, fConfig, 1000 * fTimeout, playerName));
}
}
}

View File

@ -0,0 +1,49 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.BaseState;
public class ErrorScreen extends MenuScreen {
private final String message;
private TextButton buttonBack;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(BaseState.class, BaseState::dismissErrorScreen);
sfxClick();
}
}
};
public ErrorScreen(WizardGame game, String message) {
super(game);
this.message = message;
}
@Override
public void show() {
super.show();
buttonBack = new TextButton(messages.get("menu.error.back"), skin);
var label = new Label(message, skin);
var content = new VerticalGroup();
content.setPosition(WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT*0.5f);
content.addActor(label);
content.addActor(buttonBack);
buttonBack.addListener(listener);
stage.addActor(content);
stage.addCaptureListener(new KeyboardFocusManager(buttonBack));
}
}

View File

@ -0,0 +1,285 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.actors.CardActor;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.Menu;
import eu.jonahbauer.wizard.client.libgdx.util.WizardAssetManager;
import eu.jonahbauer.wizard.common.model.Card;
public class InstructionScreen extends MenuScreen {
private static final int MAX_PAGE = 3;
private TextButton buttonBack;
private VerticalGroup content;
private ScrollPane scrollPane;
private TextButton nextPageButton;
private TextButton previousPageButton;
private int currentPage = 0;
private TextureAtlas atlas;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(Menu.class, Menu::showMenuScreen);
sfxClick();
} else if (actor == nextPageButton) {
currentPage = MathUtils.clamp(currentPage + 1, 0, MAX_PAGE);
showPage(currentPage);
sfxClick();
} else if (actor == previousPageButton) {
currentPage = MathUtils.clamp(currentPage - 1, 0, MAX_PAGE);
showPage(currentPage);
sfxClick();
}
}
};
public InstructionScreen(WizardGame game) {
super(game);
}
@Override
protected void load() {
super.load();
assets.loadGame();
assets.finishLoading();
atlas = assets.get(WizardAssetManager.ATLAS_GAME);
}
@Override
public void show() {
super.show();
content = new VerticalGroup().left().grow().pad(20);
getTitle().moveBy(0, 80);
getButtonGroup().moveBy(0, - 80);
previousPageButton = new TextButton(messages.get("menu.instruction.previousPageButton"), skin);
previousPageButton.addListener(listener);
getButtonGroup().addActor(previousPageButton);
buttonBack = new TextButton(messages.get("menu.instruction.back"), skin);
buttonBack.addListener(listener);
getButtonGroup().addActor(buttonBack);
nextPageButton = new TextButton(messages.get("menu.instruction.nextPageButton"), skin);
nextPageButton.addListener(listener);
getButtonGroup().addActor(nextPageButton);
scrollPane = new ScrollPane(content, skin);
scrollPane.setPosition(0.175f * WizardGame.WIDTH, 0.2f * WizardGame.HEIGHT);
scrollPane.setSize(0.65f * WizardGame.WIDTH, 400 + 0.1f * WizardGame.HEIGHT + 80);
stage.addActor(scrollPane);
stage.addCaptureListener(new KeyboardFocusManager(scrollPane, previousPageButton, buttonBack, nextPageButton));
showFirstPage();
}
private void showPage(int page) {
switch (page) {
case 0 -> showFirstPage();
case 1 -> showSecondPage();
case 2 -> showThirdPage();
case 3 -> showFourthPage();
default -> throw new AssertionError();
}
}
private void showFirstPage() {
currentPage = 0;
reset();
startSection("menu.instruction.main.title");
startSubsection("menu.instruction.main.welcome.title");
addParagraph("menu.instruction.main.welcome.par0");
startSubsection("menu.instruction.main.general.title");
addParagraph("menu.instruction.main.general.par0");
}
private void showSecondPage() {
currentPage = 1;
reset();
startSection("menu.instruction.standard.title");
addParagraph("menu.instruction.standard.par0");
var colors = new HorizontalGroup().space(10).pad(10).center();
for (Card.Suit suit : Card.Suit.values()) {
if (suit != Card.Suit.NONE) {
VerticalGroup suitGroup = new VerticalGroup().space(10);
suitGroup.addActor(new CardActor(suit, atlas));
suitGroup.addActor(new Label(switch (suit) {
case RED -> messages.get("menu.instruction.suit.red");
case GREEN -> messages.get("menu.instruction.suit.green");
case BLUE -> messages.get("menu.instruction.suit.blue");
case YELLOW -> messages.get("menu.instruction.suit.yellow");
default -> suit.toString();
}, skin));
colors.addActor(suitGroup);
}
}
content.addActor(colors);
addParagraph("menu.instruction.standard.par1");
var specialCards = new HorizontalGroup().space(10).pad(10).center();
var wizard = new VerticalGroup();
wizard.addActor(new CardActor(Card.BLUE_WIZARD, atlas));
wizard.addActor(new Label(messages.get("menu.instruction.wizard"), skin));
specialCards.addActor(wizard);
var jester = new VerticalGroup();
jester.addActor(new CardActor(Card.BLUE_JESTER, atlas));
jester.addActor(new Label(messages.get("menu.instruction.jester"), skin));
specialCards.addActor(jester);
content.addActor(specialCards);
addParagraph("menu.instruction.standard.par2");
startSubsection("menu.instruction.standard.trick.title");
addParagraph("menu.instruction.standard.trick.par0");
startSubsection("menu.instruction.standard.special_cases.title");
addParagraph("menu.instruction.standard.special_cases.par0");
startSubsection("menu.instruction.standard.scoring.title");
addParagraph("menu.instruction.standard.scoring.par0");
}
private void showThirdPage() {
currentPage = 2;
reset();
startSection("menu.instruction.variant.title");
addParagraph("menu.instruction.variant.intro");
Table table = new Table(skin).padTop(10);
table.defaults().space(10.0f).left().top();
table.columnDefaults(1).grow();
table.add(messages.get("menu.instruction.variant.default"));
table.add(messages.get("menu.instruction.variant.default.description")).getActor().setWrap(true);
table.row();
table.add(messages.get("menu.instruction.variant.defaultpm1"));
table.add(messages.get("menu.instruction.variant.defaultpm1.description")).getActor().setWrap(true);
table.row();
table.add(messages.get("menu.instruction.variant.anniversary2016"));
table.add(messages.get("menu.instruction.variant.anniversary2016.description")).getActor().setWrap(true);
table.row();
table.add(messages.get("menu.instruction.variant.anniversary2016pm1"));
table.add(messages.get("menu.instruction.variant.anniversary2016pm1.description")).getActor().setWrap(true);
table.row();
table.add(messages.get("menu.instruction.variant.anniversary2021"));
table.add(messages.get("menu.instruction.variant.anniversary2021.description")).getActor().setWrap(true);
table.row();
table.add(messages.get("menu.instruction.variant.anniversary2021pm1"));
table.add(messages.get("menu.instruction.variant.anniversary2021pm1.description")).getActor().setWrap(true);
table.row();
content.addActor(table);
}
private void showFourthPage() {
currentPage = 3;
reset();
startSection("menu.instruction.special_card.title");
addParagraph("menu.instruction.special_card.intro");
Table table = new Table(skin).padTop(10);
table.defaults().space(10.0f).left().top();
table.columnDefaults(1).grow();
table.add(new CardActor((Card.DRAGON), atlas));
table.add(messages.get("menu.instruction.special_card.dragon")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.FAIRY), atlas));
table.add(messages.get("menu.instruction.special_card.fairy")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.BOMB), atlas));
table.add(messages.get("menu.instruction.special_card.bomb")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.WEREWOLF), atlas));
table.add(messages.get("menu.instruction.special_card.werewolf")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.JUGGLER), atlas));
table.add(messages.get("menu.instruction.special_card.juggler")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.CLOUD), atlas));
table.add(messages.get("menu.instruction.special_card.cloud")).getActor().setWrap(true);
table.row();
table.add(new CardActor((Card.CHANGELING), atlas));
table.add(messages.get("menu.instruction.special_card.changeling")).getActor().setWrap(true);
table.row();
content.addActor(table);
}
private void reset() {
updateButtons();
content.clearChildren();
scrollPane.setScrollY(0);
scrollPane.updateVisualScroll();
}
private void updateButtons() {
previousPageButton.setDisabled(currentPage == 0);
nextPageButton.setDisabled(currentPage == MAX_PAGE);
}
private void startSection(String title) {
if (content.hasChildren()) {
var spacerPre = new Actor();
spacerPre.setHeight(20);
spacerPre.setName("spacer");
content.addActor(spacerPre);
}
var label = new Label(messages.get(title).trim(), skin, "enchanted");
label.setAlignment(Align.center);
content.addActor(label);
var spacerPost = new Actor();
spacerPost.setHeight(10);
spacerPost.setName("spacer");
content.addActor(spacerPost);
}
private void startSubsection(String title) {
if (content.hasChildren() && !"spacer".equals(content.getChild(content.getChildren().size - 1).getName())) {
var spacerPre = new Actor();
spacerPre.setHeight(10);
spacerPre.setName("spacer");
content.addActor(spacerPre);
}
var label = new Label(messages.get(title), skin);
label.setFontScale(2.0f);
label.setAlignment(Align.center);
content.addActor(label);
}
private void addParagraph(String paragraph) {
var label = new Label(messages.get(paragraph), skin);
label.setAlignment(Align.left);
label.setWrap(true);
content.addActor(label);
}
@Override
public void dispose() {
super.dispose();
assets.unloadGame();
}
}

View File

@ -0,0 +1,34 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
public class LoadingScreen extends MenuScreen {
private final String key;
@Deprecated
public LoadingScreen(WizardGame game) {
this(game, "menu.loading.loading");
}
public LoadingScreen(WizardGame game, String key) {
super(game);
this.key = key;
}
@Override
public void show() {
super.show();
var label = new Label(messages.get(key), skin);
var content = new VerticalGroup();
content.setPosition(WizardGame.WIDTH * 0.5f, WizardGame.HEIGHT*0.5f);
content.addActor(label);
stage.addActor(content);
}
}

View File

@ -0,0 +1,242 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.state.Lobby;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import lombok.extern.log4j.Log4j2;
import java.util.UUID;
@Log4j2
public class LobbyScreen extends MenuScreen {
private TextButton buttonBack;
private TextButton buttonJoin;
private TextButton buttonRejoin;
private TextButton buttonCreate;
private TextField playerName;
private Label labelSessionName;
private Label labelSessionPlayerCount;
private Label labelSessionConfiguration;
private UUID selectedSession;
private List<SessionData> sessions;
private ScrollPane sessionListContainer;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(Lobby.class, Lobby::disconnect);
sfxClick();
} else if (actor == buttonJoin) {
join();
sfxClick();
} else if (actor == buttonCreate) {
game.getClient().execute(Lobby.class, Lobby::showCreateScreen);
sfxClick();
} else if (actor == buttonRejoin) {
game.getClient().execute(Lobby.class, Lobby::showRejoinScreen);
sfxClick();
}
}
};
public LobbyScreen(WizardGame game) {
super(game);
}
@Override
public void show() {
super.show();
buttonBack = new TextButton(messages.get("menu.lobby.back"), skin);
buttonBack.addListener(listener);
getButtonGroup().addActor(buttonBack);
buttonCreate = new TextButton(messages.get("menu.lobby.create"), skin);
buttonCreate.addListener(listener);
getButtonGroup().addActor(buttonCreate);
buttonRejoin = new TextButton(messages.get("menu.lobby.rejoin"), skin);
buttonRejoin.addListener(listener);
getButtonGroup().addActor(buttonRejoin);
buttonJoin = new TextButton(messages.get("menu.lobby.join"), skin);
buttonJoin.addListener(listener);
getButtonGroup().addActor(buttonJoin);
getButtonGroup().setWidth(0.55f * WizardGame.WIDTH);
sessions = new List<>(skin) {
@Override
public String toString(SessionData session) {
return session.getName();
}
};
sessions.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
var selected = sessions.getSelected();
updateData(selected);
}
});
sessionListContainer = new ScrollPane(sessions, skin);
sessionListContainer.layout();
sessions.addListener(new ResetErrorListener(skin, sessionListContainer));
var content = new HorizontalGroup().grow().space(20);
content.setPosition(0.25f * WizardGame.WIDTH, 0.3f * WizardGame.HEIGHT);
content.setSize(0.5f * WizardGame.WIDTH, 400);
content.addActor(new Container<>(sessionListContainer).width(0.2f * WizardGame.WIDTH).height(400));
content.addActor(createInfoTable());
content.layout();
stage.addActor(content);
stage.addCaptureListener(new KeyboardFocusManager(
sessions, playerName, buttonBack, buttonCreate, buttonRejoin, buttonJoin
));
buttonBack.setName("button_back");
buttonJoin.setName("button_join");
buttonJoin.setName("button_rejoin");
buttonCreate.setName("button_create");
sessions.setName("session_list");
playerName.setName("player_name");
labelSessionName.setName("session_name");
labelSessionConfiguration.setName("session_configuration");
labelSessionPlayerCount.setName("session_player_count");
}
private Table createInfoTable() {
float infoTableWidth = 0.3f * WizardGame.WIDTH - 20;
playerName = new TextField(game.storage.playerName, skin);
playerName.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
game.storage.playerName = playerName.getText();
}
});
playerName.setMaxLength(20);
playerName.addListener(new ResetErrorListener(skin));
labelSessionName = new Label("", skin, "textfield");
labelSessionConfiguration = new Label("", skin, "textfield");
labelSessionPlayerCount = new Label("", skin, "textfield");
labelSessionName.setEllipsis(true);
labelSessionConfiguration.setEllipsis(true);
labelSessionPlayerCount.setEllipsis(true);
var infoTable = new Table().center().left();
infoTable.columnDefaults(0).growX().width(infoTableWidth);
infoTable.setSize(infoTableWidth, 400);
infoTable.add(new Label(messages.get("menu.lobby.player_name.label"), skin)).row();
infoTable.add(playerName).row();
infoTable.add(new Label(messages.get("menu.lobby.session_name.label"), skin)).row();
infoTable.add(labelSessionName).row();
infoTable.add(new Label(messages.get("menu.lobby.session_configuration.label"), skin)).row();
infoTable.add(labelSessionConfiguration).row();
infoTable.add(new Label(messages.get("menu.lobby.session_player_count.label"), skin)).row();
infoTable.add(labelSessionPlayerCount).row();
return infoTable;
}
public void addSession(SessionData session) {
this.sessions.getItems().add(session);
this.sessions.invalidateHierarchy();
}
public void removeSession(UUID session) {
var index = indexOf(session);
if (index != -1) {
this.sessions.getItems().removeIndex(index);
this.sessions.invalidateHierarchy();
}
if (selectedSession != null && selectedSession.equals(session)) {
updateData(null);
}
}
public void modifySession(SessionData session) {
var index = indexOf(session.getUuid());
this.sessions.getItems().set(index, session);
this.sessions.invalidateHierarchy();
if (selectedSession != null && selectedSession.equals(session.getUuid())) {
updateData(session);
}
}
public void setSessions(SessionData... sessions) {
var items = this.sessions.getItems();
items.clear();
items.addAll(sessions);
this.selectedSession = null;
this.sessions.invalidateHierarchy();
}
private void updateData(SessionData data) {
if (data != null) {
labelSessionName.setText(data.getName());
labelSessionPlayerCount.setText(Integer.toString(data.getPlayerCount()));
labelSessionConfiguration.setText(data.getConfiguration().toString());
selectedSession = data.getUuid();
} else {
labelSessionName.setText("");
labelSessionPlayerCount.setText("");
labelSessionConfiguration.setText("");
selectedSession = null;
}
}
private void join() {
boolean error = false;
String playerName = this.playerName.getText();
if (playerName.isBlank()) {
log.warn("Please choose a player name");
this.playerName.setStyle(getTextFieldErrorStyle());
error = true;
}
if (selectedSession == null) {
log.warn("Please select a session.");
this.sessionListContainer.setStyle(getScrollPaneErrorStyle());
error = true;
}
if (!error) {
var client = game.getClient();
try {
client.execute(Lobby.class, (s, c) -> s.joinSession(c, selectedSession, playerName));
} catch (IllegalArgumentException e) {
// only if session is not known
log.warn(e.getMessage());
this.sessionListContainer.setStyle(getScrollPaneErrorStyle());
}
}
}
private int indexOf(UUID session) {
var items = this.sessions.getItems();
for (int i = 0; i < items.size; i++) {
if (items.get(i).getUuid().equals(session)) {
return i;
}
}
return -1;
}
}

View File

@ -0,0 +1,100 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.Menu;
public class MainMenuScreen extends MenuScreen {
private TextButton buttonPlay;
private TextButton buttonQuit;
private TextButton buttonInstruction;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonPlay) {
game.getClient().execute(Menu.class, Menu::showConnectScreen);
sfxClick();
} else if (actor == buttonQuit) {
sfxClick();
Gdx.app.exit();
} else if (actor == buttonInstruction) {
game.getClient().execute(Menu.class, Menu::showInstructionScreen);
sfxClick();
}
}
};
public MainMenuScreen(WizardGame game) {
super(game);
}
@Override
public void show() {
super.show();
int width = 160, height = 224;
int left = 384, right = 384, top = 384, bottom = 192;
var symbols = new Image[]{
new Image(skin.getRegion(UiskinAtlas.SYMBOL_0)),
new Image(skin.getRegion(UiskinAtlas.SYMBOL_1)),
new Image(skin.getRegion(UiskinAtlas.SYMBOL_2)),
new Image(skin.getRegion(UiskinAtlas.SYMBOL_3))
};
symbols[0].setPosition(left, bottom);
symbols[1].setPosition(left, WizardGame.HEIGHT - top - height);
symbols[2].setPosition(WizardGame.WIDTH - right - width, bottom);
symbols[3].setPosition(WizardGame.WIDTH - right - width, WizardGame.HEIGHT - top - height);
for (var symbol : symbols) {
symbol.setSize(width, height);
stage.addActor(symbol);
}
var buttonGroup = new VerticalGroup() {
@Override
public void layout() {
float contentHeight = 0;
for (Actor child : getChildren()) {
if (child instanceof Layout layout) {
contentHeight += layout.getPrefHeight();
} else {
contentHeight += child.getHeight();
}
}
space(Math.max(0, (getHeight() - contentHeight) / (getChildren().size - 1)));
super.layout();
}
}.center().fill();
buttonGroup.setPosition(WizardGame.WIDTH * 0.25f, 192 + 60f);
buttonGroup.setSize(WizardGame.WIDTH * 0.5f, 504 - 120f);
stage.addActor(buttonGroup);
buttonPlay = new TextButton(messages.get("menu.main.play"), skin);
buttonPlay.addListener(listener);
buttonGroup.addActor(buttonPlay);
buttonInstruction = new TextButton(messages.get("menu.main.instructions"), skin);
buttonInstruction.addListener(listener);
buttonGroup.addActor(buttonInstruction);
buttonQuit = new TextButton(messages.get("menu.main.quit"), skin);
buttonQuit.addListener(listener);
buttonGroup.addActor(buttonQuit);
stage.addCaptureListener(new KeyboardFocusManager(buttonPlay, buttonInstruction, buttonQuit));
buttonPlay.setName("button_player");
buttonInstruction.setName("button_instruction");
buttonQuit.setName("button_quit");
}
}

View File

@ -0,0 +1,92 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.Layout;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import lombok.AccessLevel;
import lombok.Getter;
public abstract class MenuScreen extends WizardScreen {
private Image[] corners;
@Getter(value = AccessLevel.PROTECTED, lazy = true)
private final HorizontalGroup buttonGroup = createButtonGroup();
@Getter(value = AccessLevel.PROTECTED, lazy = true)
private final TextField.TextFieldStyle textFieldErrorStyle = skin.get("error", TextField.TextFieldStyle.class);
@Getter(value = AccessLevel.PROTECTED, lazy = true)
private final ScrollPane.ScrollPaneStyle scrollPaneErrorStyle = skin.get("error", ScrollPane.ScrollPaneStyle.class);
@Getter(value = AccessLevel.PROTECTED, lazy = true)
private final SelectBox.SelectBoxStyle selectBoxErrorStyle = skin.get("error", SelectBox.SelectBoxStyle.class);
@Getter(AccessLevel.PROTECTED)
private Image title;
public MenuScreen(WizardGame game) {
super(game);
}
@Override
public void show() {
super.show();
corners = new Image[4];
corners[0] = new Image(skin.getRegion(UiskinAtlas.CORNER_TOP_LEFT));
corners[1] = new Image(skin.getRegion(UiskinAtlas.CORNER_BOTTOM_LEFT));
corners[2] = new Image(skin.getRegion(UiskinAtlas.CORNER_BOTTOM_RIGHT));
corners[3] = new Image(skin.getRegion(UiskinAtlas.CORNER_TOP_RIGHT));
for (var corner : corners) {
stage.addActor(corner);
}
title = new Image(skin.getRegion(UiskinAtlas.TITLE));
title.setSize(810, 192);
title.setPosition(555, WizardGame.HEIGHT - 192 - 96);
stage.addActor(title);
}
private HorizontalGroup createButtonGroup() {
var group = new HorizontalGroup() {
@Override
public void layout() {
float contentWidth = 0;
for (Actor child : getChildren()) {
if (child instanceof Layout layout) {
contentWidth += layout.getPrefWidth();
} else {
contentWidth += child.getWidth();
}
}
space(Math.max(0, (getWidth() - contentWidth) / (getChildren().size - 1)));
super.layout();
}
@Override
protected void sizeChanged() {
setPosition((WizardGame.WIDTH - getWidth()) / 2f, getY());
}
};
group.setSize(WizardGame.WIDTH * 0.45f, 125);
group.setY(WizardGame.HEIGHT * 0.15f);
group.center();
stage.addActor(group);
return group;
}
@Override
public void resize(int width, int height) {
super.resize(width, height);
var worldWidth = viewport.getWorldWidth();
var worldHeight = viewport.getWorldHeight();
var offsetX = (worldWidth - WizardGame.WIDTH) / 2;
var offsetY = (worldHeight - WizardGame.HEIGHT) / 2;
corners[0].setPosition(- offsetX, worldHeight - corners[0].getHeight() - offsetY);
corners[1].setPosition(- offsetX, - offsetY);
corners[2].setPosition(worldWidth - corners[2].getWidth() - offsetX, - offsetY);
corners[3].setPosition(worldWidth - corners[3].getWidth() - offsetX, worldHeight - corners[3].getHeight() - offsetY);
}
}

View File

@ -0,0 +1,150 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener;
import eu.jonahbauer.wizard.client.libgdx.state.Lobby;
import lombok.extern.log4j.Log4j2;
import java.util.UUID;
@Log4j2
public class RejoinScreen extends MenuScreen {
private TextButton buttonBack;
private TextButton buttonContinue;
private TextField sessionUUID;
private TextField playerUUID;
private TextField secret;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonBack) {
game.getClient().execute(Lobby.class, Lobby::showListScreen);
sfxClick();
} else if (actor == buttonContinue) {
rejoin();
sfxClick();
}
}
};
public RejoinScreen(WizardGame game) {
super(game);
}
@Override
public void show() {
super.show();
var credentials = game.storage.credentials;
buttonBack = new TextButton(messages.get("menu.rejoin.back"), skin);
buttonBack.addListener(listener);
getButtonGroup().addActor(buttonBack);
buttonContinue = new TextButton(messages.get("menu.rejoin.continue"), skin);
buttonContinue.addListener(listener);
getButtonGroup().addActor(buttonContinue);
var errorListener = new ResetErrorListener(skin);
sessionUUID = new TextField(credentials != null ? credentials.session().toString() : null , skin);
sessionUUID.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.5f);
sessionUUID.setSize(0.4f * WizardGame.WIDTH, 64);
sessionUUID.addListener(errorListener);
sessionUUID.setProgrammaticChangeEvents(true);
playerUUID = new TextField(credentials != null ? credentials.player().toString() : null, skin);
playerUUID.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.45f);
playerUUID.setSize(0.4f * WizardGame.WIDTH, 64);
playerUUID.addListener(errorListener);
secret = new TextField(credentials != null ? credentials.secret() : null, skin);
secret.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.4f);
secret.setSize(0.4f * WizardGame.WIDTH, 64);
secret.addListener(errorListener);
var contentTable = new Table(skin).center().left();
contentTable.columnDefaults(0).growX().width(0.4f * WizardGame.WIDTH - 20);
contentTable.setSize(0.4f * WizardGame.WIDTH - 20, 400);
contentTable.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f);
contentTable.add(messages.get("menu.rejoin.session_uuid.label")).row();
contentTable.add(sessionUUID).row();
contentTable.add(messages.get("menu.rejoin.player_uuid.label")).row();
contentTable.add(playerUUID).row();
contentTable.add(messages.get("menu.rejoin.player_secret.label")).row();
contentTable.add(secret).row();
stage.addActor(contentTable);
stage.addCaptureListener(new KeyboardFocusManager(sessionUUID, playerUUID, secret, buttonBack, buttonContinue));
buttonBack.setName("button_back");
buttonContinue.setName("button_continue");
sessionUUID.setName("session_uuid");
playerUUID.setName("player_uuid");
secret.setName("player_secret");
}
private void rejoin() {
boolean error = false;
String sessionUUIDText = this.sessionUUID.getText();
UUID sessionUUID = null;
if (sessionUUIDText.isBlank()) {
log.warn("Please enter the session uuid.");
this.sessionUUID.setStyle(getTextFieldErrorStyle());
error = true;
} else {
try {
sessionUUID = UUID.fromString(sessionUUIDText);
} catch (IllegalArgumentException e) {
log.warn("Please enter a valid session uuid.");
this.sessionUUID.setStyle(getTextFieldErrorStyle());
error = true;
}
}
String playerUUIDText = this.playerUUID.getText();
UUID playerUUID = null;
if (playerUUIDText.isBlank()) {
log.warn("Please enter the player uuid.");
this.playerUUID.setStyle(getTextFieldErrorStyle());
error = true;
} else {
try {
playerUUID = UUID.fromString(playerUUIDText);
} catch (IllegalArgumentException e) {
log.warn("Please enter a valid player uuid.");
this.playerUUID.setStyle(getTextFieldErrorStyle());
error = true;
}
}
String playerSecret = this.secret.getText();
if (playerSecret.isBlank()) {
log.warn("Please enter the player secret.");
this.secret.setStyle(getTextFieldErrorStyle());
error = true;
}
var fPlayerUUID = playerUUID;
var fSessionUUID = sessionUUID;
if (!error) {
var client = game.getClient();
try {
client.execute(Lobby.class, (s, c) -> s.rejoinSession(c, fSessionUUID, fPlayerUUID, playerSecret));
} catch (IllegalArgumentException e) {
// only if session is not known
log.warn(e.getMessage());
this.sessionUUID.setStyle(getTextFieldErrorStyle());
}
}
}
}

View File

@ -0,0 +1,189 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.*;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager;
import eu.jonahbauer.wizard.client.libgdx.state.Session;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.model.Configuration;
import java.util.UUID;
public class WaitingScreen extends MenuScreen {
private TextButton buttonLeave;
private TextButton buttonReady;
private Label labelSessionName;
private Label labelSessionUUID;
private Label labelSessionConfiguration;
private Label labelPlayerName;
private List<PlayerData> players;
private final ChangeListener listener = new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
if (actor == buttonLeave) {
game.getClient().execute(Session.class, Session::leave);
sfxClick();
} else if (actor == buttonReady) {
game.getClient().execute(Session.class, Session::toggleReady);
sfxClick();
}
}
};
public WaitingScreen(WizardGame game) {
super(game);
}
@Override
public void show() {
super.show();
buttonLeave = new TextButton(messages.get("menu.waiting.leave"), skin);
buttonLeave.addListener(listener);
getButtonGroup().addActor(buttonLeave);
buttonReady = new TextButton(messages.get("menu.waiting.ready"), skin);
buttonReady.addListener(listener);
getButtonGroup().addActor(buttonReady);
getButtonGroup().setWidth(0.55f * WizardGame.WIDTH);
players = new List<>(skin) {
private final TextureRegion ready = skin.getRegion(UiskinAtlas.READY);
private final TextureRegion notReady = skin.getRegion(UiskinAtlas.NOT_READY);
@Override
public String toString(PlayerData player) {
return player.getName();
}
@Override
@SuppressWarnings("SuspiciousNameCombination")
protected GlyphLayout drawItem(Batch batch, BitmapFont font, int index, PlayerData item, float x, float y, float width) {
String string = toString(item);
var height = font.getCapHeight();
if (item.isReady()) {
batch.draw(ready, x, y - height, height, height);
} else {
batch.draw(notReady, x, y - height, height, height);
}
return font.draw(batch, string, x + height + 8, y, 0, string.length(), width - height - 8, Align.left, false, "...");
}
};
var listContainer = new ScrollPane(players, skin);
listContainer.layout();
var content = new HorizontalGroup().grow().space(20);
content.setPosition(0.25f * WizardGame.WIDTH, 0.3f * WizardGame.HEIGHT);
content.setSize(0.5f * WizardGame.WIDTH, 400);
content.addActor(new Container<>(listContainer).width(0.2f * WizardGame.WIDTH).height(400));
content.addActor(createInfoTable());
content.layout();
stage.addActor(content);
stage.addCaptureListener(new KeyboardFocusManager(buttonLeave, buttonReady));
buttonLeave.setName("button_leave");
buttonReady.setName("button_ready");
labelSessionName.setName("session_name");
labelSessionUUID.setName("session_uuid");
labelSessionConfiguration.setName("session_configuration");
labelPlayerName.setName("player_name");
}
private Table createInfoTable() {
float infoTableWidth = 0.3f * WizardGame.WIDTH - 20;
labelSessionName = new Label("", skin, "textfield");
labelSessionUUID = new Label("", skin, "textfield");
labelSessionConfiguration = new Label("", skin, "textfield");
labelPlayerName = new Label("", skin, "textfield");
labelSessionName.setEllipsis(true);
labelSessionUUID.setEllipsis(true);
labelSessionConfiguration.setEllipsis(true);
labelPlayerName.setEllipsis(true);
var infoTable = new Table(skin).center().left();
infoTable.columnDefaults(0).growX().width(infoTableWidth);
infoTable.setSize(infoTableWidth, 400);
infoTable.add(messages.get("menu.waiting.session_name.label")).row();
infoTable.add(labelSessionName).row();
infoTable.add(messages.get("menu.waiting.session_uuid.label")).row();
infoTable.add(labelSessionUUID).row();
infoTable.add(messages.get("menu.waiting.session_configuration.label")).row();
infoTable.add(labelSessionConfiguration).row();
infoTable.add(messages.get("menu.waiting.player_name.label")).row();
infoTable.add(labelPlayerName).row();
return infoTable;
}
public void setSending(boolean sending) {
buttonReady.setDisabled(sending);
}
public void setReady(boolean ready) {
buttonReady.setText(messages.get(ready ? "menu.waiting.not_ready" : "menu.waiting.ready"));
}
public void addPlayer(PlayerData player) {
this.players.getItems().add(player);
this.players.invalidateHierarchy();
}
public void removePlayer(UUID player) {
var index = indexOf(player);
if (index != -1) {
this.players.getItems().removeIndex(index);
this.players.invalidateHierarchy();
}
}
public void modifyPlayer(PlayerData data) {
var index = indexOf(data.getUuid());
this.players.getItems().set(index, data);
this.players.invalidateHierarchy();
}
public void setPlayers(PlayerData... players) {
var items = this.players.getItems();
items.clear();
items.addAll(players);
this.players.invalidateHierarchy();
}
public void setSession(UUID uuid, String name, Configuration configuration) {
this.labelSessionName.setText(name);
this.labelSessionUUID.setText(uuid.toString());
this.labelSessionConfiguration.setText(configuration.toString());
}
public void setPlayerName(String name) {
this.labelPlayerName.setText(name);
}
private int indexOf(UUID player) {
var items = this.players.getItems();
for (int i = 0; i < items.size; i++) {
if (items.get(i).getUuid().equals(player)) {
return i;
}
}
return -1;
}
}

View File

@ -0,0 +1,117 @@
package eu.jonahbauer.wizard.client.libgdx.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.I18NBundle;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.listeners.AutoFocusListener;
import eu.jonahbauer.wizard.client.libgdx.listeners.ButtonKeyListener;
import eu.jonahbauer.wizard.client.libgdx.util.WizardAssetManager;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
public abstract class WizardScreen implements Screen {
protected final WizardGame game;
protected final WizardAssetManager assets;
protected Skin skin;
protected I18NBundle messages;
protected Stage stage;
protected Viewport viewport;
private Image background;
private Sound sfxClick;
protected float offsetX;
protected float offsetY;
protected float worldWidth;
protected float worldHeight;
protected WizardScreen(WizardGame game) {
this.game = game;
this.assets = game.assets;
}
@MustBeInvokedByOverriders
protected void load() {
messages = assets.get(WizardAssetManager.MESSAGES);
skin = assets.get(WizardAssetManager.SKIN);
skin.getAll(BitmapFont.class).forEach(entry -> {
entry.value.getRegion().getTexture().setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
});
viewport = new ExtendViewport(WizardGame.WIDTH, WizardGame.HEIGHT);
stage = new Stage(viewport);
stage.addListener(new ButtonKeyListener());
stage.addListener(new AutoFocusListener());
stage.setDebugAll(WizardGame.DEBUG);
Gdx.input.setInputProcessor(stage);
sfxClick = assets.get(WizardAssetManager.SFX_CLICK);
}
@Override
@MustBeInvokedByOverriders
public void show() {
load();
background = new Image(skin.getRegion(UiskinAtlas.BACKGROUND));
background.setScaling(Scaling.fill);
background.setPosition(0,0);
stage.addActor(background);
}
@Override
public final void render(float delta) {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT | (Gdx.graphics.getBufferFormat().coverageSampling?GL20.GL_COVERAGE_BUFFER_BIT_NV:0));
stage.act(delta);
stage.draw();
}
@Override
public void resize(int width, int height) {
viewport.update(width, height, true);
worldWidth = viewport.getWorldWidth();
worldHeight = viewport.getWorldHeight();
offsetX = (worldWidth - WizardGame.WIDTH) / 2;
offsetY = (worldHeight - WizardGame.HEIGHT) / 2;
stage.getCamera().position.x -= offsetX;
stage.getCamera().position.y -= offsetY;
background.setBounds(-offsetX, -offsetY, worldWidth, worldHeight);
}
@Override
public void pause() {}
@Override
public void resume() {}
@Override
public void hide() {}
@Override
@MustBeInvokedByOverriders
public void dispose() {
stage.dispose();
}
protected void sfxClick() {
sfxClick.play(0.6f);
}
}

View File

@ -0,0 +1,28 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import java.util.Optional;
@Log4j2
public abstract class Awaiting extends BaseState implements ClientState {
private static final int TIMEOUT_MILLIS = 10_000;
@Override
@SneakyThrows
@MustBeInvokedByOverriders
public Optional<ClientState> onEnter(Client client) {
client.timeout(this, TIMEOUT_MILLIS);
return Optional.empty();
}
@Override
public Optional<ClientState> onTimeout(Client client) {
log.error("Timed out. Returning to menu.");
return Optional.of(new Menu());
}
}

View File

@ -0,0 +1,47 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.extern.log4j.Log4j2;
import java.util.Optional;
@Log4j2
public final class AwaitingConnection extends Awaiting {
@Override
public Optional<ClientState> onEnter(Client client) {
log.info("Awaiting connection...");
if (!client.isError()) showScreen(client);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onOpen(Client client) {
log.info("Connection established.");
return Optional.of(new AwaitingJoinLobby());
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
return unexpectedMessage(client, message);
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
log.error("Connection could not be established. (code={}, reason={}, remote={})", code, reason, remote);
showErrorScreen(client, "Connection could not be established. (code=%d, reason=%s, remote=%b)".formatted(code, reason, remote));
return Optional.of(new Menu());
}
private void showScreen(Client client) {
client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.connecting"));
}
}

View File

@ -0,0 +1,70 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen;
import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.messages.server.StartingGameMessage;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import java.util.*;
@Log4j2
@RequiredArgsConstructor
public final class AwaitingGameLog extends BaseState {
private static final int TIMEOUT_MILLIS = 10_000;
private final UUID self;
private final UUID session;
private final String sessionName;
private final Configuration configuration;
private final LinkedHashMap<UUID, String> players;
private final List<ObserverMessage> messages = new ArrayList<>();
private boolean started = false;
@Override
public Optional<ClientState> onEnter(Client client) {
log.info("Waiting for game log...");
if (!client.isError()) showScreen(client);
client.timeout(this, TIMEOUT_MILLIS);
return Optional.empty();
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (!started) {
if (message instanceof StartingGameMessage) {
started = true;
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
} else if (message instanceof GameMessage gameMessage) {
messages.add(gameMessage.getObserverMessage());
return Optional.empty();
} else if (message instanceof AckMessage) {
var game = new Game(self, session, sessionName, configuration, players);
game.init(messages);
return Optional.of(game);
} else {
return unexpectedMessage(client, message);
}
}
private void showScreen(Client client) {
client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.rejoining"));
}
}

View File

@ -0,0 +1,40 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.messages.server.SessionListMessage;
import lombok.extern.log4j.Log4j2;
import java.util.Optional;
@Log4j2
public final class AwaitingJoinLobby extends Awaiting {
@Override
public Optional<ClientState> onEnter(Client client) {
log.info("Waiting for session list...");
if (!client.isError()) showScreen(client);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionListMessage list) {
log.info("There are {} open sessions.", list.getSessions().size());
return Optional.of(new Lobby(list.getSessions()));
} else {
return unexpectedMessage(client, message);
}
}
private void showScreen(Client client) {
client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.joining_lobby"));
}
}

View File

@ -0,0 +1,106 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen;
import eu.jonahbauer.wizard.client.libgdx.util.SavedData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.UUID;
@Log4j2
@Getter
@RequiredArgsConstructor
public final class AwaitingJoinSession extends Awaiting {
private final @Nullable UUID session;
private final @NotNull String sessionName;
private final @NotNull Configuration configuration;
private final @NotNull Source source;
@Override
public Optional<ClientState> onEnter(Client client) {
log.info("Waiting for acknowledgment...");
if (!client.isError()) showScreen(client);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionJoinedMessage joined) {
var session = joined.getSession();
if (this.session != null && !this.session.equals(session)) {
return unexpectedMessage(client, message);
} else {
var players = joined.getPlayers();
var player = joined.getPlayer();
var secret = joined.getSecret();
log.info("There are {} players in this session.", players.size());
log.info("Your uuid is {}.", player);
log.info("Your secret is {}.", secret);
client.getGame().storage.credentials = new SavedData.SessionCredentials(session, player, secret);
if (source == Source.REJOIN) {
var playerMap = new LinkedHashMap<UUID, String>();
players.forEach(p -> playerMap.put(p.getUuid(), p.getName()));
return Optional.of(new AwaitingGameLog(
player,
session,
sessionName,
configuration,
playerMap
));
} else {
return Optional.of(new Session(
new SessionData(session, sessionName, -1, configuration),
players,
player
));
}
}
} else if (message instanceof NackMessage nack) {
switch (nack.getCode()) {
case NackMessage.GAME_ALREADY_STARTED -> log.error("Game has already started.");
case NackMessage.SESSION_FULL -> log.error("The session is full.");
case NackMessage.SESSION_NOT_FOUND -> log.error("Session not found.");
case NackMessage.PLAYER_NAME_TAKEN -> log.error("Player name already taken.");
case NackMessage.PLAYER_NAME_NOT_ALLOWED -> log.error("Player name not allowed.");
case NackMessage.SESSION_NAME_TAKEN -> log.error("Session name already taken.");
case NackMessage.SESSION_NAME_NOT_ALLOWED -> log.error("Session name not allowed.");
default -> log.error("Nack {}: {}", nack.getCode(), nack.getMessage());
}
showErrorScreen(client, nack.getMessage());
return Optional.of(new AwaitingJoinLobby());
} else if (message instanceof SessionModifiedMessage || message instanceof SessionRemovedMessage) {
// drop
log.debug("Dropped message {}.", message);
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
private void showScreen(Client client) {
client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.joining_session"));
}
public enum Source {
JOIN, CREATE, REJOIN
}
}

View File

@ -0,0 +1,54 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.WizardGame;
import eu.jonahbauer.wizard.client.libgdx.screens.ErrorScreen;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.extern.log4j.Log4j2;
import java.util.Optional;
@Log4j2
public abstract class BaseState implements ClientState {
@Override
public Optional<ClientState> onEnter(Client context) {
return ClientState.super.onEnter(context);
}
@Override
public Optional<ClientState> onOpen(Client client) {
throw new IllegalStateException();
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
if (remote) {
log.error("Lost connection (code={}, reason={})", code, reason);
} else {
log.info("Connection closed (code={}, reason={})", code, reason);
}
return Optional.of(new Menu());
}
protected Optional<ClientState> unexpectedMessage(Client client, ServerMessage message) {
// return to menu on unexpected message
log.fatal("Unexpected message {}. Returning to menu.", message);
showErrorScreen(client, "Unexpected message %s. Returning to menu.".formatted(message));
return Optional.of(new Menu());
}
@Deprecated
public void showErrorScreen(Client client, String message) {
WizardGame game = client.getGame();
client.setError(true);
game.setScreen(new ErrorScreen(game, message));
}
public Optional<ClientState> dismissErrorScreen(Client client) {
client.setError(false);
return onErrorDismissed(client);
}
protected abstract Optional<ClientState> onErrorDismissed(Client client);
}

View File

@ -0,0 +1,16 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.common.machine.TimeoutState;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import java.util.Optional;
public interface ClientState extends TimeoutState<ClientState, Client> {
Optional<ClientState> onOpen(Client client);
Optional<ClientState> onMessage(Client client, ServerMessage message);
Optional<ClientState> onClose(Client client, int code, String reason, boolean remote);
}

View File

@ -0,0 +1,626 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.actions.overlay.InteractionOverlay;
import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen;
import eu.jonahbauer.wizard.client.libgdx.util.Pair;
import eu.jonahbauer.wizard.common.messages.client.InteractionMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.observer.*;
import eu.jonahbauer.wizard.common.messages.player.*;
import eu.jonahbauer.wizard.common.messages.server.AckMessage;
import eu.jonahbauer.wizard.common.messages.server.GameMessage;
import eu.jonahbauer.wizard.common.messages.server.NackMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import eu.jonahbauer.wizard.common.model.Card;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Action.*;
@Log4j2
@Getter
@RequiredArgsConstructor
public final class Game extends BaseState {
private final UUID self;
private final UUID session;
private final String sessionName;
private final Configuration configuration;
private List<ObserverMessage> pendingMessages;
private final LinkedHashMap<UUID, String> players;
private final Map<Integer, @Unmodifiable Map<UUID, Integer>> scores = new HashMap<>();
private final Map<Integer, Map<UUID, Integer>> predictions = new HashMap<>();
private int round = -1;
private final Map<UUID, List<Card>> hands = new HashMap<>();
private final Map<UUID, List<List<Card>>> tricks = new HashMap<>();
private int trick = -1;
private final List<Pair<UUID, Card>> stack = new ArrayList<>();
private Interaction currentInteraction;
private int pendingClearActivePlayer = 0;
private Card trumpCard;
private Card.Suit trumpSuit;
private @Nullable GameScreen gameScreen;
private final AtomicBoolean sending = new AtomicBoolean(false);
private boolean juggling;
private Card juggleCard;
private boolean werewolf;
private int finishing = 0;
public void init(List<ObserverMessage> messages) {
pendingMessages = messages;
}
@Override
public Optional<ClientState> onEnter(Client client) {
var out = handlePendingMessages(client);
if (out.isPresent()) return out;
gameScreen = new GameScreen(client.getGame(), self, players);
client.getGame().setScreen(gameScreen);
updateScreen();
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
return Optional.of(new Menu());
}
//<editor-fold desc="onMessage">
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
try {
if (message instanceof GameMessage game) {
var observerMessage = game.getObserverMessage();
return onMessage(client, observerMessage);
} else if (message instanceof NackMessage nack) {
return onNackMessage(client, nack);
} else if (message instanceof AckMessage) {
onAckMessage();
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
} finally {
executeDelayedFinishInteraction();
}
}
private Optional<ClientState> onMessage(Client client, ObserverMessage message) {
if (finishing != 0) return onMessageWhileFinishing(client, message);
if (message instanceof StateMessage state) {
switch (state.getState()) {
case "starting_round" -> {
return onStartRound(client);
}
case "starting_trick" -> onStartTrick();
case "juggling" -> onJuggle();
case "finishing_round" -> onFinishingRound();
case "finished" -> {
return onFinished();
}
case "error" -> {
return onError(client);
}
}
} else if (message instanceof HandMessage hand) {
onHandMessage(hand.getPlayer(), hand.getHand());
} else if (message instanceof PredictionMessage prediction) {
onPredictionMessage(prediction.getPlayer(), prediction.getPrediction());
} else if (message instanceof TrumpMessage trump) {
onTrumpMessage(trump.getCard(), trump.getSuit());
} else if (message instanceof TrickMessage trick) {
onTrickMessage(trick.getPlayer(), trick.getCards());
} else if (message instanceof CardMessage card) {
onCardMessage(card.getPlayer(), card.getCard());
} else if (message instanceof ScoreMessage score) {
onScoreMessage(score.getPoints(), true);
} else if (message instanceof UserInputMessage input) {
onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout());
} else if (message instanceof TimeoutMessage) {
onTimeoutMessage();
} else {
return unexpectedMessage(client, new GameMessage(message));
}
return Optional.empty();
}
private Optional<ClientState> onMessageWhileFinishing(Client client, ObserverMessage message) {
if (finishing == 1) { // last "finishing_round" has been received
if (message instanceof ScoreMessage score) {
onScoreMessage(score.getPoints(), false);
return Optional.empty();
} else if (message instanceof StateMessage state && "finishing".equals(state.getState())) {
finishing++;
return Optional.empty();
}
} else if (finishing == 2) { // "finishing" has been received
if (message instanceof ScoreMessage) {
return Optional.empty();
} else if (message instanceof StateMessage state && "finished".equals(state.getState())) {
onFinished();
finishing++;
return returnToSession();
}
}
return unexpectedMessage(client, new GameMessage(message));
}
private Optional<ClientState> onStartRound(Client client) {
if (isLastRound()) {
log.fatal("Cannot start round {} with {} players", round + 1, players.size());
return unexpectedMessage(client, new GameMessage(new StateMessage("starting_round")));
}
log.info("Round {} is starting...", round + 1);
round ++;
tricks.clear();
trumpSuit = null;
trumpCard = null;
stack.clear();
trick = -1;
if (gameScreen != null) gameScreen.startRound(round);
return Optional.empty();
}
private void onStartTrick() {
log.info("Trick {} is starting...", trick + 1);
trick ++;
stack.clear();
finishInteraction();
if (gameScreen != null) gameScreen.startTrick();
}
private void onJuggle() {
juggling = true;
juggleCard = null;
}
private void onFinishingRound() {
if (isLastRound()) finishing = 1; // start finish procedure
}
private Optional<ClientState> onFinished() {
log.info("The game has finished.");
if (gameScreen != null) {
gameScreen.showScoreOverlay(true);
}
return returnToSession();
}
private Optional<ClientState> onError(Client client) {
log.error("The game has finished with an error.");
showErrorScreen(client, "The game has finished with an error.");
return returnToSession();
}
private void onHandMessage(@NotNull UUID player, @Unmodifiable @NotNull List<@NotNull Card> hand) {
checkPlayer(player);
log.info("{} hand cards are: {}", nameOf(player, true, true), hand);
if (juggling) checkActivePlayer(player, JUGGLE_CARD);
finishInteraction();
hands.put(player, new ArrayList<>(hand));
if (gameScreen != null) {
gameScreen.setSelectedCard(null);
gameScreen.setHand(player, hand, juggling);
}
juggling = false;
}
private void onPredictionMessage(@NotNull UUID player, @Range(from = 0, to = Integer.MAX_VALUE) int prediction) {
checkPlayer(player);
checkActivePlayer(player, MAKE_PREDICTION, CHANGE_PREDICTION);
log.info("{} predicted: {}", nameOf(player, true, false), prediction);
boolean changed = currentInteraction != null && currentInteraction.action() == CHANGE_PREDICTION;
finishInteraction();
predictions.computeIfAbsent(round, r -> new HashMap<>()).put(player, prediction);
if (gameScreen != null) gameScreen.addPrediction(round, player, prediction, changed);
}
private void onTrumpMessage(@Nullable Card trumpCard, @Nullable Card.Suit trumpSuit) {
if (trumpCard == null) {
log.info("There is no trump in this round.");
} else {
log.info("The trump suit is {} ({}).", trumpSuit != null ? trumpSuit : "yet to be determined.", trumpCard);
}
var player = currentInteraction != null && currentInteraction.action() == PICK_TRUMP ? currentInteraction.player() : null;
finishInteraction();
this.trumpCard = trumpCard;
this.trumpSuit = trumpSuit;
if (trumpCard == Card.WEREWOLF && trumpSuit == null) {
werewolf = true;
} else {
werewolf = false;
if (gameScreen != null) gameScreen.showTrumpOverlay(player, trumpCard, trumpSuit);
}
}
private void onTrickMessage(@NotNull UUID player, @Unmodifiable @NotNull List<@NotNull Card> cards) {
checkPlayer(player);
log.info("This trick {} goes to {}.", cards, nameOf(player));
this.stack.clear();
var playerTricks = this.tricks.computeIfAbsent(player, p -> new ArrayList<>());
playerTricks.add(cards);
if (gameScreen != null) gameScreen.finishTrick(player, playerTricks.size());
}
private void onCardMessage(@NotNull UUID player, @NotNull Card card) {
checkPlayer(player);
checkActivePlayer(player, PLAY_CARD);
log.info("{} played {}.", nameOf(player, true, false), card);
finishInteraction();
this.stack.add(Pair.of(player, card));
var handCard = switch (card) {
case CHANGELING_JESTER, CHANGELING_WIZARD -> Card.CHANGELING;
case JUGGLER_BLUE, JUGGLER_GREEN, JUGGLER_RED, JUGGLER_YELLOW -> Card.JUGGLER;
case CLOUD_BLUE, CLOUD_GREEN, CLOUD_RED, CLOUD_YELLOW -> Card.CLOUD;
default -> card;
};
var hand = this.hands.get(player);
if (hand != null) {
hand.remove(handCard);
}
if (gameScreen != null) gameScreen.playCard(player, handCard, card);
}
private void onScoreMessage(@Unmodifiable Map<@NotNull UUID, @NotNull Integer> points, boolean showOverlay) {
log.info("The scores are as follows: " + points);
scores.put(round, points);
if (gameScreen != null) {
gameScreen.addScores(round, points);
if (showOverlay) gameScreen.showScoreOverlay(false);
}
}
private void onUserInputMessage(@Nullable UUID player, @NotNull UserInputMessage.Action action, long timeout) {
checkPlayer(player);
log.info(
"Waiting for input {} from {}. (times out at {})",
action,
nameOf(player),
LocalDateTime.ofInstant(Instant.ofEpochMilli(timeout), ZoneId.systemDefault())
);
if (action == UserInputMessage.Action.SYNC) {
if (gameScreen != null) gameScreen.sync();
} else {
currentInteraction = new Interaction(player, action, timeout);
showCurrentInteraction();
if (werewolf && action == PICK_TRUMP) {
werewolf = false;
}
}
}
private void onTimeoutMessage() {
log.info("The previous interaction timed out.");
delayedFinishInteraction();
if (gameScreen != null) gameScreen.timeout();
}
private Optional<ClientState> onNackMessage(Client client, @NotNull NackMessage nack) {
sending.set(false);
if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
juggleCard = null;
}
int code = nack.getCode();
if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) {
log.error(nack.getMessage());
if (gameScreen != null) {
gameScreen.addMessage(true, "game.message.literal", nack.getMessage());
gameScreen.ready(false);
}
return Optional.empty();
} else {
return unexpectedMessage(client, nack);
}
}
private void onAckMessage() {
log.info("OK");
sending.set(false);
if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) {
if (gameScreen != null) gameScreen.setSelectedCard(juggleCard);
juggleCard = null;
}
if (gameScreen != null) gameScreen.ready(true);
}
//</editor-fold>
//<editor-fold desc="Screen Callbacks" defaultstate="collapsed">
public Optional<ClientState> onCardClicked(Client client, @NotNull Card card) {
assert gameScreen != null;
if (isActive()) {
if (currentInteraction.action() == PLAY_CARD) {
if (card == Card.CLOUD || card == Card.JUGGLER || card == Card.CHANGELING) {
var oldOverlay = currentInteraction.overlay();
if (oldOverlay != null) oldOverlay.close();
currentInteraction.overlay(gameScreen.showSpecialCardOverlay(card, currentInteraction.timeout()));
} else {
send(client, new PlayCardMessage(card));
}
return Optional.empty();
} else if (currentInteraction.action() == JUGGLE_CARD) {
if (send(client, new JuggleMessage(card))) {
juggleCard = card;
}
return Optional.empty();
}
}
gameScreen.addMessage(true, "game.message.nack.not_allowed");
return Optional.empty();
}
public Optional<ClientState> onSuitClicked(Client client, @NotNull Card.Suit suit) {
assert gameScreen != null;
if (isActive() && currentInteraction.action() == PICK_TRUMP) {
send(client, new PickTrumpMessage(suit));
} else {
gameScreen.addMessage(true, "game.message.nack.not_allowed");
}
return Optional.empty();
}
public Optional<ClientState> onPredictionMade(Client client, int prediction) {
assert gameScreen != null;
if (isActive()) {
if (currentInteraction.action() == MAKE_PREDICTION || currentInteraction.action() == CHANGE_PREDICTION) {
send(client, new PredictMessage(prediction));
return Optional.empty();
}
}
gameScreen.addMessage(true, "game.message.nack.not_allowed");
return Optional.empty();
}
public Optional<ClientState> sync(Client client) {
assert gameScreen != null;
send(client, new ContinueMessage());
return Optional.empty();
}
//</editor-fold>
public Optional<ClientState> returnToMenu() {
return Optional.of(new Menu());
}
private Optional<ClientState> returnToSession() {
return Optional.of(new Session(
new SessionData(session, sessionName, -1, configuration),
players.entrySet().stream()
.map(entry -> new PlayerData(entry.getKey(), entry.getValue(), false))
.toList(),
self,
true
));
}
/**
* Sends a message. Only one message may be sent at a time until without receiving an answer.
* @return {@code true} iff the message was sent
*/
private boolean send(Client client, PlayerMessage message) {
if (message instanceof ContinueMessage || !sending.getAndSet(true)) {
client.send(new InteractionMessage(message));
return true;
} else {
if (gameScreen != null) gameScreen.addMessage(true, "game.message.nack.too_fast");
return false;
}
}
//<editor-fold desc="Interactions" defaultstate="collapsed">
/**
* Checks whether some action from the player is expected.
*/
private boolean isActive() {
return currentInteraction != null && (currentInteraction.player() == null || self.equals(currentInteraction.player()));
}
private void showCurrentInteraction() {
if (gameScreen == null) return;
var player = currentInteraction.player();
var action = currentInteraction.action();
var timeout = currentInteraction.timeout();
gameScreen.setActivePlayer(player, action, timeout);
if (player != null && werewolf && action == PICK_TRUMP) {
gameScreen.swapTrumpCard(player);
}
if (isActive()) {
switch (action) {
case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf));
case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout));
case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(round).get(player), timeout ));
case PLAY_CARD -> gameScreen.setPersistentMessage("game.message.play_card.self");
}
}
}
/**
* Close the interaction overlay associated with the current interaction and reset the current interaction.
*/
private void finishInteraction() {
if (currentInteraction != null) {
var overlay = currentInteraction.overlay();
if (overlay != null) overlay.close();
}
currentInteraction = null;
if (gameScreen != null) gameScreen.clearActivePlayer();
}
/**
* UI-wise equivalent to {@link #finishInteraction()} but information about the interaction is kept until after the
* next message.
*/
private void delayedFinishInteraction() {
pendingClearActivePlayer = 2;
if (currentInteraction != null) {
var overlay = currentInteraction.overlay();
if (overlay != null) overlay.close();
}
if (gameScreen != null) gameScreen.clearActivePlayer();
}
private void executeDelayedFinishInteraction() {
if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) {
finishInteraction();
}
}
//</editor-fold>
private Optional<ClientState> handlePendingMessages(Client client) {
if (pendingMessages != null) {
for (var message : pendingMessages) {
var result = onMessage(client, message);
if (result.isPresent()) {
return result;
}
executeDelayedFinishInteraction();
}
pendingMessages = null;
}
return Optional.empty();
}
private void updateScreen() {
if (gameScreen == null) return;
gameScreen.clear();
gameScreen.setPredictions(predictions);
gameScreen.setScores(scores);
gameScreen.setTrump(trumpCard, trumpSuit);
gameScreen.setStack(stack);
var hand = hands.get(self);
if (hand != null) {
gameScreen.setHand(self, hand, false);
}
if (currentInteraction != null) {
showCurrentInteraction();
} else {
gameScreen.clearActivePlayer();
}
}
private boolean isLastRound() {
return round + 1 >= 60 / getPlayers().size();
}
//<editor-fold desc="Logging" defaultState="collapsed">
/**
* Checks whether the given player is known and logs errors.
*/
private void checkPlayer(UUID player) {
if (player != null && !players.containsKey(player)) {
log.error("Unknown player {}.", player);
}
}
/**
* Checks whether one of the given actions is currently expected from the given player and logs errors.
*/
private void checkActivePlayer(UUID player, UserInputMessage.Action...actions) {
if (currentInteraction != null && (currentInteraction.player() == null || currentInteraction.player().equals(player))) {
for (var action : actions) {
if (currentInteraction.action() == action) {
return;
}
}
}
log.warn("Received message does not match the previous user input message.");
}
/**
* Returns the name of the given player.
*/
private String nameOf(UUID player) {
return nameOf(player, false, false);
}
/**
* Returns the name of the given player, optionally with a capitalized first letter and/or a possessive suffix ("'s")
*/
private String nameOf(UUID player, boolean capitalize, boolean possessive) {
if (player == null) {
return (capitalize ? "A" : "a") + "ll players" + (possessive ? "'" : "");
} else if (self.equals(player)) {
return (capitalize ? "Y" : "y") + "ou" + (possessive ? "r" : "");
} else {
return players.get(player) + (possessive ? "'s" : "");
}
}
//</editor-fold>
@Data
@Accessors(fluent = true)
public static final class Interaction {
private final UUID player;
private final UserInputMessage.Action action;
private final long timeout;
private WeakReference<InteractionOverlay> overlay;
public void overlay(InteractionOverlay overlay) {
this.overlay = new WeakReference<>(overlay);
}
public InteractionOverlay overlay() {
if (overlay == null) return null;
else return overlay.get();
}
}
}

View File

@ -0,0 +1,122 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.*;
import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.RejoinMessage;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static eu.jonahbauer.wizard.client.libgdx.state.AwaitingJoinSession.Source.*;
public final class Lobby extends BaseState {
private final Map<UUID, SessionData> sessions = new HashMap<>();
private LobbyScreen lobbyScreen;
public Lobby(Collection<SessionData> list) {
list.forEach(s -> sessions.put(s.getUuid(), s));
}
@Override
public Optional<ClientState> onEnter(Client client) {
if (!client.isError()) showListScreen(client);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showListScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof SessionCreatedMessage created) {
var session = created.getSession();
sessions.put(session.getUuid(), session);
if (lobbyScreen != null) {
lobbyScreen.addSession(session);
}
return Optional.empty();
} else if (message instanceof SessionRemovedMessage removed) {
var session = removed.getSession();
sessions.remove(session);
if (lobbyScreen != null) {
lobbyScreen.removeSession(session);
}
return Optional.empty();
} else if (message instanceof SessionModifiedMessage modified) {
var session = modified.getSession();
sessions.put(session.getUuid(), session);
if (lobbyScreen != null) {
lobbyScreen.modifySession(session);
}
return Optional.empty();
} else if (message instanceof SessionListMessage list) {
list.getSessions().forEach(s -> sessions.put(s.getUuid(), s));
if (lobbyScreen != null) {
lobbyScreen.setSessions(list.getSessions().toArray(new SessionData[0]));
}
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
public Optional<ClientState> disconnect(@SuppressWarnings("unused") Client client) {
return Optional.of(new Menu());
}
public Optional<ClientState> createSession(Client client, @NotNull String sessionName, @NotNull Configuration config, long timeout, @NotNull String playerName) {
client.send(new CreateSessionMessage(sessionName, playerName, timeout, config));
return Optional.of(new AwaitingJoinSession(null, sessionName, config, CREATE));
}
public Optional<ClientState> joinSession(Client client, UUID sessionUUID, String playerName) {
var session = sessions.get(sessionUUID);
if (session != null) {
client.send(new JoinSessionMessage(sessionUUID, playerName));
return Optional.of(new AwaitingJoinSession(session.getUuid(), session.getName(), session.getConfiguration(), JOIN));
} else {
throw new IllegalArgumentException("Session does not exist.");
}
}
public Optional<ClientState> rejoinSession(Client client, @NotNull UUID sessionUUID, @NotNull UUID playerUUID, @NotNull String secret) {
var session = sessions.get(sessionUUID);
if (session != null) {
client.send(new RejoinMessage(sessionUUID, playerUUID, secret));
return Optional.of(new AwaitingJoinSession(sessionUUID, session.getName(), session.getConfiguration(), REJOIN));
} else {
throw new IllegalArgumentException("Session does not exist.");
}
}
public Optional<ClientState> showCreateScreen(Client client) {
var game = client.getGame();
lobbyScreen = null;
game.setScreen(new CreateGameScreen(game));
return Optional.empty();
}
public Optional<ClientState> showListScreen(Client client) {
var game = client.getGame();
lobbyScreen = new LobbyScreen(game);
game.setScreen(lobbyScreen);
lobbyScreen.setSessions(sessions.values().toArray(SessionData[]::new));
return Optional.empty();
}
public Optional<ClientState> showRejoinScreen(Client client) {
var game = client.getGame();
lobbyScreen = null;
game.setScreen(new RejoinScreen(game));
return Optional.empty();
}
}

View File

@ -0,0 +1,73 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.ClientSocket;
import eu.jonahbauer.wizard.client.libgdx.screens.ConnectScreen;
import eu.jonahbauer.wizard.client.libgdx.screens.InstructionScreen;
import eu.jonahbauer.wizard.client.libgdx.screens.MainMenuScreen;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.java_websocket.framing.CloseFrame;
import java.net.URI;
import java.util.Optional;
@Log4j2
public final class Menu extends BaseState {
@Override
@SneakyThrows
public Optional<ClientState> onEnter(Client client) {
if (client.getSocket() != null && client.getSocket().isOpen()) {
client.getSocket().close(CloseFrame.GOING_AWAY);
}
if (!client.isError()) showMenuScreen(client);
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showMenuScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
// it is possible that there are messages still queued after
// returning to the menu as a result of a previous message
log.debug("Dropped message {}.", message);
return Optional.empty();
}
@Override
public Optional<ClientState> onClose(Client client, int code, String reason, boolean remote) {
super.onClose(client, code, reason, remote);
return Optional.empty();
}
public Optional<ClientState> showConnectScreen(Client client) {
var game = client.getGame();
game.setScreen(new ConnectScreen(game));
return Optional.empty();
}
public Optional<ClientState> showMenuScreen(Client client) {
var game = client.getGame();
game.setScreen(new MainMenuScreen(game));
return Optional.empty();
}
public Optional<ClientState> showInstructionScreen(Client client) {
var game = client.getGame();
game.setScreen(new InstructionScreen(game));
return Optional.empty();
}
public Optional<ClientState> connect(Client client, URI uri) {
ClientSocket socket = new ClientSocket(uri);
client.setSocket(socket);
socket.connect();
return Optional.of(new AwaitingConnection());
}
}

View File

@ -0,0 +1,142 @@
package eu.jonahbauer.wizard.client.libgdx.state;
import eu.jonahbauer.wizard.client.libgdx.Client;
import eu.jonahbauer.wizard.client.libgdx.screens.WaitingScreen;
import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage;
import eu.jonahbauer.wizard.common.messages.client.ReadyMessage;
import eu.jonahbauer.wizard.common.messages.data.PlayerData;
import eu.jonahbauer.wizard.common.messages.data.SessionData;
import eu.jonahbauer.wizard.common.messages.server.*;
import eu.jonahbauer.wizard.common.model.Configuration;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import java.util.*;
@Log4j2
@Getter
public final class Session extends BaseState {
private WaitingScreen sessionScreen;
private final UUID self;
private final UUID session;
private final String sessionName;
private final Configuration configuration;
private final LinkedHashMap<UUID, PlayerData> players = new LinkedHashMap<>();
private final boolean dontSwitchScreen;
private boolean sending;
public Session(SessionData session, Collection<PlayerData> players, UUID self) {
this(session, players, self, false);
}
public Session(SessionData session, Collection<PlayerData> players, UUID self, boolean dontSwitchScreen) {
this.session = session.getUuid();
this.sessionName = session.getName();
this.configuration = session.getConfiguration();
players.forEach(p -> this.players.put(p.getUuid(), p));
this.dontSwitchScreen = dontSwitchScreen;
this.self = self;
}
@Override
public Optional<ClientState> onEnter(Client client) {
if (!dontSwitchScreen) {
showInfoScreen(client);
}
return super.onEnter(client);
}
@Override
public Optional<ClientState> onErrorDismissed(Client client) {
showInfoScreen(client);
return Optional.empty();
}
@Override
public Optional<ClientState> onMessage(Client client, ServerMessage message) {
if (message instanceof PlayerJoinedMessage join) {
var player = join.getPlayer();
log.info("Player {} joined the session.", player.getName());
players.put(player.getUuid(), player);
if (sessionScreen != null) sessionScreen.addPlayer(player);
return Optional.empty();
} else if (message instanceof PlayerLeftMessage leave) {
var uuid = leave.getPlayer();
var player = players.remove(uuid);
log.info("Player {} left the session.", player.getName());
if (sessionScreen != null) sessionScreen.removePlayer(uuid);
return Optional.empty();
} else if (message instanceof PlayerModifiedMessage modified) {
var player = modified.getPlayer();
log.info("Player {} was modified.", player.getName());
players.put(player.getUuid(), player);
if (sessionScreen != null) {
sessionScreen.modifyPlayer(player);
if (self.equals(player.getUuid())) {
sessionScreen.setReady(player.isReady());
}
}
return Optional.empty();
} else if (message instanceof StartingGameMessage) {
var players = new LinkedHashMap<UUID, String>();
this.players.forEach((uuid, player) -> players.put(uuid, player.getName()));
return Optional.of(new Game(self, session, sessionName, configuration, players));
} else if (sending && message instanceof NackMessage nack) {
// TODO display error
log.error(nack.getMessage());
sending = false;
if (sessionScreen != null) sessionScreen.setSending(false);
return Optional.empty();
} else if (sending && message instanceof AckMessage) {
sending = false;
if (sessionScreen != null) sessionScreen.setSending(false);
return Optional.empty();
} else {
return unexpectedMessage(client, message);
}
}
public Optional<ClientState> setReady(Client client, boolean ready) {
if (sending) {
log.warn("Please slow down");
} else {
sending = true;
if (sessionScreen != null) sessionScreen.setSending(true);
client.send(new ReadyMessage(ready));
}
return Optional.empty();
}
public Optional<ClientState> toggleReady(Client client) {
return setReady(client, !isReady());
}
public Optional<ClientState> leave(Client client) {
client.send(new LeaveSessionMessage());
return Optional.of(new AwaitingJoinLobby());
}
public Optional<ClientState> showInfoScreen(Client client) {
sessionScreen = new WaitingScreen(client.getGame());
client.getGame().setScreen(sessionScreen);
sessionScreen.setPlayers(players.values().toArray(new PlayerData[0]));
sessionScreen.setReady(players.get(self).isReady());
sessionScreen.setPlayerName(getName());
sessionScreen.setSession(session, sessionName, configuration);
sessionScreen.setSending(sending);
return Optional.empty();
}
private boolean isReady() {
return players.get(self).isReady();
}
private String getName() {
return players.get(self).getName();
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.wizard.client.libgdx.util;
import lombok.experimental.UtilityClass;
@UtilityClass
public class AnimationTimings {
private static final float FACTOR = 1;
public static final float JUGGLE = 0.25f * FACTOR;
public static final float STACK_EXPAND = 0.25f * FACTOR;
public static final float STACK_COLLAPSE = 0.25f * FACTOR;
public static final float STACK_FINISH_MOVE = 0.25f * FACTOR;
public static final float STACK_FINISH_ROTATE = 0.1f * FACTOR;
public static final float STACK_FINISH_FADE = 0.5f * FACTOR;
public static final float STACK_HOLD = 0.5f * FACTOR;
public static final float WEREWOLF_SWAP = 0.25f * FACTOR;
public static final float PAD_OF_TRUTH_EXPAND = 0.25f * FACTOR;
public static final float PAD_OF_TRUTH_COLLAPSE = 0.25f * FACTOR;
public static final float HAND_LAYOUT = 0.15f * FACTOR;
public static final float OVERLAY_HOLD = 3f * FACTOR;
public static final float OVERLAY_SHARED_ELEMENT = .3f * FACTOR;
public static final float MESSAGE_HOLD = 1.5f * FACTOR;
public static final float MESSAGE_FADE = 0.5f * FACTOR;
}

View File

@ -0,0 +1,75 @@
package eu.jonahbauer.wizard.client.libgdx.util;
import eu.jonahbauer.wizard.common.model.Card;
import lombok.experimental.UtilityClass;
import java.util.Map;
@UtilityClass
public class CardUtil {
@SuppressWarnings("RedundantTypeArguments")
private static final Map<Card, Card.Suit> DEFAULT_SUITES = Map.<Card, Card.Suit>ofEntries(
Map.entry(Card.BLUE_1, Card.Suit.BLUE),
Map.entry(Card.BLUE_2, Card.Suit.BLUE),
Map.entry(Card.BLUE_3, Card.Suit.BLUE),
Map.entry(Card.BLUE_4, Card.Suit.BLUE),
Map.entry(Card.BLUE_5, Card.Suit.BLUE),
Map.entry(Card.BLUE_6, Card.Suit.BLUE),
Map.entry(Card.BLUE_7, Card.Suit.BLUE),
Map.entry(Card.BLUE_8, Card.Suit.BLUE),
Map.entry(Card.BLUE_9, Card.Suit.BLUE),
Map.entry(Card.BLUE_10, Card.Suit.BLUE),
Map.entry(Card.BLUE_11, Card.Suit.BLUE),
Map.entry(Card.BLUE_12, Card.Suit.BLUE),
Map.entry(Card.BLUE_13, Card.Suit.BLUE),
Map.entry(Card.RED_1, Card.Suit.RED),
Map.entry(Card.RED_2, Card.Suit.RED),
Map.entry(Card.RED_3, Card.Suit.RED),
Map.entry(Card.RED_4, Card.Suit.RED),
Map.entry(Card.RED_5, Card.Suit.RED),
Map.entry(Card.RED_6, Card.Suit.RED),
Map.entry(Card.RED_7, Card.Suit.RED),
Map.entry(Card.RED_8, Card.Suit.RED),
Map.entry(Card.RED_9, Card.Suit.RED),
Map.entry(Card.RED_10, Card.Suit.RED),
Map.entry(Card.RED_11, Card.Suit.RED),
Map.entry(Card.RED_12, Card.Suit.RED),
Map.entry(Card.RED_13, Card.Suit.RED),
Map.entry(Card.GREEN_1, Card.Suit.GREEN),
Map.entry(Card.GREEN_2, Card.Suit.GREEN),
Map.entry(Card.GREEN_3, Card.Suit.GREEN),
Map.entry(Card.GREEN_4, Card.Suit.GREEN),
Map.entry(Card.GREEN_5, Card.Suit.GREEN),
Map.entry(Card.GREEN_6, Card.Suit.GREEN),
Map.entry(Card.GREEN_7, Card.Suit.GREEN),
Map.entry(Card.GREEN_8, Card.Suit.GREEN),
Map.entry(Card.GREEN_9, Card.Suit.GREEN),
Map.entry(Card.GREEN_10, Card.Suit.GREEN),
Map.entry(Card.GREEN_11, Card.Suit.GREEN),
Map.entry(Card.GREEN_12, Card.Suit.GREEN),
Map.entry(Card.GREEN_13, Card.Suit.GREEN),
Map.entry(Card.YELLOW_1, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_2, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_3, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_4, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_5, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_6, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_7, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_8, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_9, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_10, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_11, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_12, Card.Suit.YELLOW),
Map.entry(Card.YELLOW_13, Card.Suit.YELLOW),
Map.entry(Card.RED_JESTER, Card.Suit.NONE),
Map.entry(Card.GREEN_JESTER, Card.Suit.NONE),
Map.entry(Card.BLUE_JESTER, Card.Suit.NONE),
Map.entry(Card.YELLOW_JESTER, Card.Suit.NONE),
Map.entry(Card.BOMB, Card.Suit.NONE),
Map.entry(Card.FAIRY, Card.Suit.NONE)
);
public Card.Suit getDefaultTrumpSuit(Card card) {
return card == null ? Card.Suit.NONE : DEFAULT_SUITES.get(card);
}
}

View File

@ -0,0 +1,25 @@
package eu.jonahbauer.wizard.client.libgdx.util;
import java.util.Map;
public record Pair<F,S>(F first, S second) implements Map.Entry<F,S> {
public static <F,S> Pair<F,S> of(F first, S second) {
return new Pair<>(first, second);
}
@Override
public F getKey() {
return first();
}
@Override
public S getValue() {
return second();
}
@Override
public S setValue(S value) {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,14 @@
package eu.jonahbauer.wizard.client.libgdx.util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
public class SavedData {
public @NotNull String uri = "wss://webdev.jonahbauer.eu/wizard/";
public @Nullable String playerName;
public @Nullable SessionCredentials credentials;
public record SessionCredentials(@NotNull UUID session, @NotNull UUID player, @NotNull String secret) {}
}

View File

@ -0,0 +1,48 @@
package eu.jonahbauer.wizard.client.libgdx.util;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.I18NBundleLoader;
import com.badlogic.gdx.assets.loaders.SkinLoader;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.I18NBundle;
import eu.jonahbauer.wizard.client.libgdx.GameAtlas;
import eu.jonahbauer.wizard.client.libgdx.UiskinAtlas;
import lombok.experimental.Delegate;
import java.util.Locale;
public class WizardAssetManager {
public static final String SKIN = "uiskin.json";
public static final String ATLAS_SKIN = UiskinAtlas.$PATH;
public static final String ATLAS_GAME = GameAtlas.$PATH;
public static final String SFX_CLICK = "button_click_s.mp3";
public static final String MUSIC_BACKGROUND = "background.mp3";
public static final String CURSOR = "cursor.png";
public static final String MESSAGES = "i18n/messages";
@Delegate
private final AssetManager manager = new AssetManager();
public void loadShared() {
manager.load(CURSOR, Pixmap.class);
manager.load(MESSAGES, I18NBundle.class, new I18NBundleLoader.I18NBundleParameter(Locale.getDefault()));
manager.load(SKIN, Skin.class, new SkinLoader.SkinParameter(ATLAS_SKIN));
manager.load(MUSIC_BACKGROUND, Music.class);
manager.load(SFX_CLICK, Sound.class);
}
public void loadGame() {
manager.load(ATLAS_GAME, TextureAtlas.class);
}
public void unloadGame() {
manager.unload(ATLAS_GAME);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Some files were not shown because too many files have changed in this diff Show More