Compare commits
104 Commits
62154687c0
...
854e2a914b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
854e2a914b | ||
![]() |
bfe865a06e | ||
![]() |
f918e23544 | ||
8313e4ed5b | |||
98dbf5cb74 | |||
82db2bdab1 | |||
96ed5ac483 | |||
a9d9d209b9 | |||
86307e997b | |||
ae8f33e598 | |||
dcfea1854c | |||
111d089b1f | |||
607d6d104e | |||
1e4c772a66 | |||
ccc0dc2972 | |||
![]() |
0f61b25d4f | ||
e015f2af81 | |||
3e9e0edd70 | |||
e094a210f4 | |||
![]() |
850c5aa985 | ||
![]() |
96b999dd39 | ||
![]() |
ee0f2384be | ||
![]() |
114cae0d6c | ||
![]() |
07633da4ee | ||
a5b00ac0a1 | |||
891e47ffec | |||
d26d6646c5 | |||
d2468557b5 | |||
e44e0db63e | |||
3b9becbbda | |||
2e9549e4e0 | |||
c2a81febb9 | |||
0e6658c2d9 | |||
93e341d445 | |||
b5d82ac8de | |||
155049830b | |||
94ac10e93b | |||
b20f300260 | |||
8c719a8835 | |||
3adaeffe2d | |||
![]() |
ee2367643d | ||
bf87f4af1c | |||
f7d753e7e0 | |||
4eb1b61e7b | |||
298167d4c9 | |||
7a25d3b881 | |||
50fe4b7681 | |||
a977e4dca1 | |||
238c67ce48 | |||
2d3477ffc5 | |||
62b6c3fd81 | |||
62f04dfded | |||
6fe8a70d9e | |||
1e5ed45c63 | |||
44162b5bf3 | |||
64152219a0 | |||
![]() |
99c4f68893 | ||
![]() |
5e428d1301 | ||
![]() |
e5e67a3418 | ||
![]() |
1fc7103d66 | ||
![]() |
17ec7a1a90 | ||
![]() |
4a6e1e81e4 | ||
![]() |
fb400afdc2 | ||
![]() |
3bd95c0b61 | ||
![]() |
a9e4644b06 | ||
![]() |
442fc6c7d0 | ||
![]() |
c7344a502f | ||
![]() |
4cd5e90e2b | ||
![]() |
3179a17b36 | ||
![]() |
db07bfa453 | ||
![]() |
1cc4c3eeb9 | ||
![]() |
766e923713 | ||
![]() |
34f6d9848b | ||
![]() |
6fc1c9429e | ||
![]() |
5cf4bc506f | ||
![]() |
3e9b9d1f31 | ||
![]() |
96a2d43aaf | ||
![]() |
9bc2d08ddd | ||
![]() |
73ff37cc38 | ||
![]() |
2b86d0d86c | ||
![]() |
7f33529e52 | ||
![]() |
4ed23e1a0f | ||
![]() |
385c6ba50a | ||
![]() |
270bc40fa3 | ||
![]() |
67bb44eb39 | ||
![]() |
544f9c0877 | ||
![]() |
36deb15f02 | ||
![]() |
a24e8b0223 | ||
![]() |
b6868db2c6 | ||
![]() |
649fd581f8 | ||
![]() |
d1acde7bde | ||
![]() |
74c5e7709a | ||
![]() |
a0b483a4f0 | ||
![]() |
f04ee9177f | ||
![]() |
5d9c958732 | ||
![]() |
4946c214be | ||
![]() |
ceb4c49e01 | ||
![]() |
7f84889a02 | ||
![]() |
7e1d15591e | ||
![]() |
2f1794973a | ||
![]() |
eba221525f | ||
![]() |
c301d08094 | ||
![]() |
bba7153801 | ||
![]() |
b32ff5aa6f |
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
46
.gitlab-ci.yml
Normal 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
74
build.gradle.kts
Normal 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
12
buildSrc/build.gradle.kts
Normal 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())
|
||||
}
|
101
buildSrc/src/main/kotlin/Dependencies.kt
Normal file
101
buildSrc/src/main/kotlin/Dependencies.kt
Normal 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"
|
||||
}
|
@ -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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
234
gradlew
vendored
Normal 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
89
gradlew.bat
vendored
Normal 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
11
settings.gradle.kts
Normal 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")
|
0
wizard-client/build.gradle.kts
Normal file
0
wizard-client/build.gradle.kts
Normal file
22
wizard-client/wizard-client-cli/build.gradle.kts
Normal file
22
wizard-client/wizard-client-cli/build.gradle.kts
Normal 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")
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package eu.jonahbauer.wizard.client.cli.commands;
|
||||
|
||||
import static picocli.CommandLine.Command;
|
||||
|
||||
@Command(name = "quit", aliases = "exit")
|
||||
public class QuitCommand {
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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)
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
20
wizard-client/wizard-client-libgdx/build.gradle.kts
Normal file
20
wizard-client/wizard-client-libgdx/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
22
wizard-client/wizard-client-libgdx/core/build.gradle.kts
Normal file
22
wizard-client/wizard-client-libgdx/core/build.gradle.kts
Normal 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)
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package eu.jonahbauer.wizard.client.libgdx.actions.overlay;
|
||||
|
||||
public interface InteractionOverlay {
|
||||
void close();
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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.
Binary file not shown.
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 |
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user