diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..9db1355
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,46 @@
+name: Publish web page
+
+on:
+ push:
+ branches: [ "release" ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v6
+
+ - name: Setup Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: 17
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6
+
+ - name: Make gradlew executable
+ run: chmod +x gradlew
+
+ - name: Build web artifacts
+ run: ./gradlew wasmJsBrowserDistribution
+
+ - name: Upload Artifact
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: composeApp/build/dist/wasmJs/productionExecutable/
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index adfa9bf..98d5e0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,6 @@
xcuserdata
!src/**/build/
local.properties
-.idea
.DS_Store
captures
.externalNativeBuild
@@ -17,3 +16,11 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
+
+# IntelliJ files
+.idea/
+!.idea/externalDependencies.xml
+
+# Nix
+.direnv
+.envrc
diff --git a/.idea/artifacts/composeApp_js.xml b/.idea/artifacts/composeApp_js.xml
new file mode 100644
index 0000000..e52ad0e
--- /dev/null
+++ b/.idea/artifacts/composeApp_js.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/composeApp/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/composeApp_jvm.xml b/.idea/artifacts/composeApp_jvm.xml
new file mode 100644
index 0000000..96d842a
--- /dev/null
+++ b/.idea/artifacts/composeApp_jvm.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/composeApp/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/artifacts/composeApp_wasm_js.xml b/.idea/artifacts/composeApp_wasm_js.xml
new file mode 100644
index 0000000..e3c9c59
--- /dev/null
+++ b/.idea/artifacts/composeApp_wasm_js.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/composeApp/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
new file mode 100644
index 0000000..85b9f87
--- /dev/null
+++ b/.idea/externalDependencies.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..be8eb1b
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index d3c5ec8..72a57bb 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,4 +11,5 @@ plugins {
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.googleServices) apply false
alias(libs.plugins.ktlint) apply false
+ kotlin("plugin.serialization") version "2.3.0" apply false
}
diff --git a/composeApp/.editorconfig b/composeApp/.editorconfig
index 4006c13..e2c1039 100644
--- a/composeApp/.editorconfig
+++ b/composeApp/.editorconfig
@@ -16,7 +16,7 @@ ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equa
ktlint_code_style = android_studio
ktlint_enum_entry_name_casing = upper_or_camel_cases
ktlint_function_signature_body_expression_wrapping = default
-ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = unset
+ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2
ktlint_ignore_back_ticked_identifier = false
ktlint_property_naming_constant_naming = screaming_snake_case
max_line_length = 100
diff --git a/composeApp/.idea/workspace.xml b/composeApp/.idea/workspace.xml
new file mode 100644
index 0000000..008f2f9
--- /dev/null
+++ b/composeApp/.idea/workspace.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1764949855968
+
+
+ 1764949855968
+
+
+
+
\ No newline at end of file
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 5cb2d90..e3926ae 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -1,7 +1,6 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
plugins {
alias(libs.plugins.kotlinMultiplatform)
@@ -11,6 +10,7 @@ plugins {
alias(libs.plugins.composeHotReload)
alias(libs.plugins.googleServices)
alias(libs.plugins.ktlint)
+ alias(libs.plugins.serialization)
}
kotlin {
@@ -47,6 +47,7 @@ kotlin {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
+ implementation(libs.ktor.client.cio)
}
commonMain.dependencies {
implementation(compose.runtime)
@@ -55,42 +56,50 @@ kotlin {
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+ implementation(libs.kotlinx.coroutines.core)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
- implementation(libs.androidx.navigation.compose)
- implementation(libs.androidx.material.icons.extended)
- implementation(project.dependencies.platform(libs.firebase.bom))
- implementation(libs.firebase.auth)
- implementation(libs.firebase.firestore)
+ implementation(libs.navigation.compose)
+ implementation(libs.material.icons.extended)
+ implementation(
+ project.dependencies.platform(libs.supabase.bom),
+ )
+ implementation(libs.supabase.auth)
+ implementation(libs.supabase.auth.compose)
+ implementation(libs.supabase.storage)
+ implementation(libs.supabase.realtime)
+ implementation(libs.supabase.postgrest)
+ implementation(libs.kotlinx.serialization)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
- implementation(libs.kotlinx.coroutinesSwing)
+ implementation(libs.kotlinx.coroutines.swing)
+ implementation(libs.ktor.client.cio)
+ }
+ jsMain {
+ dependencies {
+ implementation(libs.ktor.client.js)
+ }
+ }
+ iosMain {
+ dependencies {
+ implementation(libs.ktor.client.darwin)
+ }
}
}
-}
-tasks.getByPath("preBuild").dependsOn("ktlintFormat")
+ jvmToolchain(21)
+}
-subprojects {
- apply(plugin = "org.jlleitschuh.gradle.ktlint")
- configure {
- version.set("1.7.1")
- debug.set(true)
- verbose.set(true)
- outputToConsole.set(true)
- outputColorName.set("RED")
- ignoreFailures.set(false)
- reporters {
- reporter(ReporterType.PLAIN)
- reporter(ReporterType.CHECKSTYLE)
- }
- }
+tasks.withType().configureEach {
+ compilerOptions.jvmTarget.set(JvmTarget.JVM_21)
}
+tasks.getByPath("preBuild").dependsOn("ktlintFormat")
+
android {
namespace = "hu.mokegyesulet.it.dunestrat"
compileSdk = libs.versions.android.compileSdk.get().toInt()
diff --git a/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.android.kt b/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.android.kt
index 9d05a82..8cf263a 100644
--- a/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.android.kt
+++ b/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.android.kt
@@ -1,9 +1,17 @@
package hu.mokegyesulet.it.dunestrat
import android.os.Build
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
class AndroidPlatform : Platform {
override val name: String = "Android ${Build.VERSION.SDK_INT}"
+ override fun openUrl(url: String) {
+ // Supabase Auth handles this natively or via dedicated launcher on Android
+ }
}
actual fun getPlatform(): Platform = AndroidPlatform()
+
+@OptIn(SupabaseExperimental::class)
+actual fun getUrlLauncher(): UrlLauncher? = null
diff --git a/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/URLOpener.android.kt b/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/URLOpener.android.kt
new file mode 100644
index 0000000..57e21cf
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/URLOpener.android.kt
@@ -0,0 +1,11 @@
+package hu.mokegyesulet.it.dunestrat.util.drawmap
+
+import android.content.Intent
+import androidx.core.net.toUri
+
+actual fun openURL(url: String) {
+ val intent = Intent(Intent.ACTION_VIEW, url.toUri()).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ TODO("Kéne egy Context-et szerezni és megnyitni vele az intentet")
+}
diff --git a/composeApp/src/commonMain/composeResources/drawable/dune_icon.png b/composeApp/src/commonMain/composeResources/drawable/dune_icon.png
new file mode 100644
index 0000000..525d3a8
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/dune_icon.png differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/crysknife.svg b/composeApp/src/commonMain/composeResources/drawable/svg/crysknife.svg
new file mode 100644
index 0000000..7ea4afc
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/crysknife.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/desert.svg b/composeApp/src/commonMain/composeResources/drawable/svg/desert.svg
new file mode 100644
index 0000000..e26abf3
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/desert.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/desert_bnw.svg b/composeApp/src/commonMain/composeResources/drawable/svg/desert_bnw.svg
new file mode 100644
index 0000000..4e5c8f5
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/desert_bnw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/field-border-with-icons.svg b/composeApp/src/commonMain/composeResources/drawable/svg/field-border-with-icons.svg
new file mode 100644
index 0000000..914610b
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/field-border-with-icons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/field-border.svg b/composeApp/src/commonMain/composeResources/drawable/svg/field-border.svg
new file mode 100644
index 0000000..0ddb046
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/field-border.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-gun.svg b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-gun.svg
new file mode 100644
index 0000000..f589156
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-gun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-laser.svg b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-laser.svg
new file mode 100644
index 0000000..93a89ed
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun-laser.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/lasgun.svg b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun.svg
new file mode 100644
index 0000000..c32da7d
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/lasgun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/mountain.svg b/composeApp/src/commonMain/composeResources/drawable/svg/mountain.svg
new file mode 100644
index 0000000..16634a0
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/mountain.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/mountain_bnw.svg b/composeApp/src/commonMain/composeResources/drawable/svg/mountain_bnw.svg
new file mode 100644
index 0000000..dddc3dd
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/mountain_bnw.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/out.svg b/composeApp/src/commonMain/composeResources/drawable/svg/out.svg
new file mode 100644
index 0000000..49f462c
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/out.svg
@@ -0,0 +1,2174 @@
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/pistol-dart.svg b/composeApp/src/commonMain/composeResources/drawable/svg/pistol-dart.svg
new file mode 100644
index 0000000..8a88d84
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/pistol-dart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/pistol-gun.svg b/composeApp/src/commonMain/composeResources/drawable/svg/pistol-gun.svg
new file mode 100644
index 0000000..e45131b
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/pistol-gun.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/pistol.svg b/composeApp/src/commonMain/composeResources/drawable/svg/pistol.svg
new file mode 100644
index 0000000..bd0a5a5
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/pistol.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/spice-rotated.svg b/composeApp/src/commonMain/composeResources/drawable/svg/spice-rotated.svg
new file mode 100644
index 0000000..9e5ca1d
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/spice-rotated.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/spice.svg b/composeApp/src/commonMain/composeResources/drawable/svg/spice.svg
new file mode 100644
index 0000000..27e4e20
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/spice.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/temp.svg b/composeApp/src/commonMain/composeResources/drawable/svg/temp.svg
new file mode 100644
index 0000000..a8127f0
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/temp.svg
@@ -0,0 +1,12 @@
+
diff --git a/composeApp/src/commonMain/composeResources/drawable/svg/water.svg b/composeApp/src/commonMain/composeResources/drawable/svg/water.svg
new file mode 100644
index 0000000..d9908ec
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/drawable/svg/water.svg
@@ -0,0 +1 @@
+Eszköz 9water
\ No newline at end of file
diff --git a/composeApp/src/commonMain/composeResources/font/Dune_Rise.otf b/composeApp/src/commonMain/composeResources/font/Dune_Rise.otf
new file mode 100644
index 0000000..df025f4
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/Dune_Rise.otf differ
diff --git a/composeApp/src/commonMain/composeResources/font/metropolis_bold.otf b/composeApp/src/commonMain/composeResources/font/metropolis_bold.otf
new file mode 100644
index 0000000..f9889d0
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/metropolis_bold.otf differ
diff --git a/composeApp/src/commonMain/composeResources/font/metropolis_light.otf b/composeApp/src/commonMain/composeResources/font/metropolis_light.otf
new file mode 100644
index 0000000..f969dcc
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/metropolis_light.otf differ
diff --git a/composeApp/src/commonMain/composeResources/font/metropolis_medium.otf b/composeApp/src/commonMain/composeResources/font/metropolis_medium.otf
new file mode 100644
index 0000000..b8b917c
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/metropolis_medium.otf differ
diff --git a/composeApp/src/commonMain/composeResources/font/metropolis_normal.otf b/composeApp/src/commonMain/composeResources/font/metropolis_normal.otf
new file mode 100644
index 0000000..b02e041
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/metropolis_normal.otf differ
diff --git a/composeApp/src/commonMain/composeResources/font/metropolis_semi_bold.otf b/composeApp/src/commonMain/composeResources/font/metropolis_semi_bold.otf
new file mode 100644
index 0000000..239d69d
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/metropolis_semi_bold.otf differ
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/App.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/App.kt
index cd78057..7406d05 100644
--- a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/App.kt
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/App.kt
@@ -1,48 +1,20 @@
package hu.mokegyesulet.it.dunestrat
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.safeContentPadding
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import dune_strat.composeapp.generated.resources.Res
-import dune_strat.composeapp.generated.resources.compose_multiplatform
-import org.jetbrains.compose.resources.painterResource
+import androidx.compose.material3.MaterialTheme.colorScheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import hu.mokegyesulet.it.dunestrat.navigation.NavGraph
+import hu.mokegyesulet.it.dunestrat.ui.AppTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
- MaterialTheme {
- var showContent by remember { mutableStateOf(false) }
- Column(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.primaryContainer)
- .safeContentPadding()
- .fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally,
+ AppTheme {
+ Surface(
+ color = colorScheme.background,
) {
- Button(onClick = { showContent = !showContent }) {
- Text("Click me!")
- }
- AnimatedVisibility(showContent) {
- val greeting = remember { Greeting().greet() }
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(painterResource(Res.drawable.compose_multiplatform), null)
- Text("Compose: $greeting")
- }
- }
+ NavGraph()
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.kt
index 5d79873..810a269 100644
--- a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.kt
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.kt
@@ -1,7 +1,14 @@
package hu.mokegyesulet.it.dunestrat
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
+
interface Platform {
val name: String
+ fun openUrl(url: String)
}
expect fun getPlatform(): Platform
+
+@OptIn(SupabaseExperimental::class)
+expect fun getUrlLauncher(): UrlLauncher?
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Auth.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Auth.kt
new file mode 100644
index 0000000..50625d9
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Auth.kt
@@ -0,0 +1,9 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+import kotlinx.coroutines.flow.Flow
+
+interface Auth {
+ suspend fun logIn()
+ suspend fun logOut()
+ val authStatus: Flow
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/AuthStatus.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/AuthStatus.kt
new file mode 100644
index 0000000..2c446ef
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/AuthStatus.kt
@@ -0,0 +1,8 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+sealed class AuthStatus {
+ class Authenticated(email: String?) : AuthStatus() {
+ val email: String? = email
+ }
+ object Unauthenticated : AuthStatus()
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Repository.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Repository.kt
new file mode 100644
index 0000000..549021e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/Repository.kt
@@ -0,0 +1,35 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+import hu.mokegyesulet.it.dunestrat.model.Desert
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.model.PlayerStep
+import kotlinx.coroutines.flow.Flow
+
+interface Repository {
+ suspend fun saveGame(game: Game): Game
+ fun getGames(): Flow>
+ suspend fun getGameById(gameId: Int): Game
+
+ suspend fun saveGameState(gameState: GameState)
+ fun getGameStates(): Flow>
+ suspend fun getLatestGameStateByGameId(gameId: Int): GameState
+
+ suspend fun savePlayerStep(playerStep: PlayerStep): PlayerStep
+ fun getPlayerSteps(): Flow>
+ fun getPlayerStepsByGameStateId(gameStateId: Int): Flow>
+ suspend fun getPlayerStep(
+ gameStateId: Int,
+ playerId: Int,
+ ): PlayerStep
+
+ suspend fun saveDesert(desert: Desert): Desert
+ fun getDeserts(): Flow>
+
+ suspend fun uploadImage(
+ svg: String,
+ gameName: String,
+ gameId: String,
+ round: Int,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseAuth.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseAuth.kt
new file mode 100644
index 0000000..e1ecf2d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseAuth.kt
@@ -0,0 +1,25 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+import io.github.jan.supabase.auth.auth
+import io.github.jan.supabase.auth.providers.Google
+import io.github.jan.supabase.auth.status.SessionStatus
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+object SupabaseAuth : Auth {
+ private val auth = SupabaseClientProvider.client.auth
+
+ override suspend fun logIn() =
+ auth.signInWith(provider = Google, redirectUrl = "https://mok-it.github.io/dune-strat/")
+ override val authStatus get(): Flow = auth.sessionStatus.map { s ->
+ if (s is SessionStatus.Authenticated) {
+ AuthStatus.Authenticated(
+ s.session.user?.email,
+ )
+ } else {
+ AuthStatus.Unauthenticated
+ }
+ }
+
+ override suspend fun logOut() = auth.signOut()
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseClientProvider.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseClientProvider.kt
new file mode 100644
index 0000000..db7a733
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseClientProvider.kt
@@ -0,0 +1,26 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+import hu.mokegyesulet.it.dunestrat.getUrlLauncher
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.createSupabaseClient
+import io.github.jan.supabase.postgrest.Postgrest
+import io.github.jan.supabase.realtime.Realtime
+import io.github.jan.supabase.storage.Storage
+
+object SupabaseClientProvider {
+ @OptIn(SupabaseExperimental::class)
+ private val _client = createSupabaseClient(
+ supabaseUrl = "https://lnwvuwepwaexwybselsf.supabase.co",
+ supabaseKey = "sb_publishable_Y8MlS_tbyIRvvsvbHvb1_g_wbyg6EfI",
+ ) {
+ install(io.github.jan.supabase.auth.Auth) {
+ getUrlLauncher()?.let {
+ urlLauncher = it
+ }
+ }
+ install(Postgrest)
+ install(Realtime)
+ install(Storage)
+ }
+ val client get() = _client
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseRepository.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseRepository.kt
new file mode 100644
index 0000000..69189c6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/SupabaseRepository.kt
@@ -0,0 +1,135 @@
+package hu.mokegyesulet.it.dunestrat.backend
+
+import hu.mokegyesulet.it.dunestrat.backend.entities.*
+import hu.mokegyesulet.it.dunestrat.model.Desert
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.model.PlayerStep
+import hu.mokegyesulet.it.dunestrat.util.StringNormalizer
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.postgrest.from
+import io.github.jan.supabase.postgrest.postgrest
+import io.github.jan.supabase.postgrest.query.filter.FilterOperation
+import io.github.jan.supabase.postgrest.query.filter.FilterOperator
+import io.github.jan.supabase.postgrest.rpc
+import io.github.jan.supabase.realtime.selectAsFlow
+import io.github.jan.supabase.storage.storage
+import io.ktor.http.ContentType
+import io.ktor.utils.io.core.toByteArray
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+object SupabaseRepository : Repository {
+ val supabase = SupabaseClientProvider.client
+ const val GAME_TABLE = GameDatabaseEntity.TABLE_NAME
+ const val GAME_STATE_TABLE = GameStateDatabaseEntity.TABLE_NAME
+ const val STEP_TABLE = PlayerStepDatabaseEntity.TABLE_NAME
+ const val DESERT_TABLE = DesertDatabaseEntity.TABLE_NAME
+ const val SVG_BUCKET_NAME = "svgs"
+
+ val svgBucket = supabase.storage.from(SVG_BUCKET_NAME)
+
+ @OptIn(SupabaseExperimental::class)
+ override fun getGames(): Flow> =
+ supabase.from(GAME_TABLE).selectAsFlow(GameDatabaseEntity::id).map { list ->
+ list.map { it.toGame() }
+ }
+
+ override suspend fun saveGame(game: Game): Game =
+ supabase.from(GAME_TABLE).insert(game.toDatabaseEntity()) {
+ select()
+ }.decodeSingle().toGame()
+
+ override suspend fun getGameById(gameId: Int): Game = supabase.from(GAME_TABLE)
+ .select { filter { GameDatabaseEntity::id eq gameId } }
+ .decodeList().firstOrNull() ?.toGame()
+ ?: throw IllegalStateException("No game found with id: $gameId")
+
+ @OptIn(SupabaseExperimental::class)
+ override fun getGameStates(): Flow> =
+ supabase.from(GAME_STATE_TABLE).selectAsFlow(GameStateDatabaseEntity::id).map { list ->
+ list.map { it.toGameState() }
+ }
+
+ @OptIn(SupabaseExperimental::class)
+ override suspend fun saveGameState(gameState: GameState) {
+ supabase.postgrest.rpc(
+ function = "save_gamestate",
+ parameters = GameStateSaveDatabaseEntity(
+ gameState.toDatabaseEntity(),
+ gameState.players.map {
+ it.id
+ },
+ ),
+ )
+ }
+
+ override suspend fun getLatestGameStateByGameId(gameId: Int): GameState =
+ supabase.from(GAME_STATE_TABLE)
+ .select {
+ filter {
+ GameStateDatabaseEntity::gameId eq gameId
+ }
+ }
+ .decodeList().maxByOrNull { it.index }?.toGameState()
+ ?: throw IllegalStateException("No game state found for game id: $gameId")
+
+ @OptIn(SupabaseExperimental::class)
+ override fun getPlayerSteps(): Flow> =
+ supabase.from(STEP_TABLE).selectAsFlow(PlayerStepDatabaseEntity::id).map { list ->
+ list.map { it.toPlayerStep() }
+ }
+
+ @OptIn(SupabaseExperimental::class)
+ override fun getPlayerStepsByGameStateId(gameStateId: Int): Flow> =
+ supabase.from(STEP_TABLE).selectAsFlow(
+ PlayerStepDatabaseEntity::id,
+ filter = FilterOperation("game_state_id", FilterOperator.EQ, gameStateId),
+ ).map { list ->
+ list.map { it.toPlayerStep() }
+ }
+
+ @OptIn(SupabaseExperimental::class)
+ override suspend fun getPlayerStep(
+ gameStateId: Int,
+ playerId: Int,
+ ): PlayerStep = supabase.from(STEP_TABLE).select {
+ filter {
+ PlayerStepDatabaseEntity::gameStateId eq gameStateId
+ PlayerStepDatabaseEntity::playerId eq playerId
+ }
+ }
+ .decodeSingle().toPlayerStep()
+
+ override suspend fun savePlayerStep(playerStep: PlayerStep): PlayerStep =
+ supabase.from(STEP_TABLE).upsert(playerStep.toDatabaseEntity()) {
+ select()
+ }.decodeSingle().toPlayerStep()
+
+ override suspend fun saveDesert(desert: Desert): Desert =
+ supabase.from(DESERT_TABLE).insert(desert.toDatabaseEntity()) {
+ select()
+ }.decodeSingle().toDesert()
+
+ @OptIn(SupabaseExperimental::class)
+ override fun getDeserts(): Flow> =
+ supabase.from(DESERT_TABLE).selectAsFlow(DesertDatabaseEntity::id).map { list ->
+ list.map { it.toDesert() }
+ }
+
+ override suspend fun uploadImage(
+ svg: String,
+ gameName: String,
+ gameId: String,
+ round: Int,
+ ) {
+ val gameString = StringNormalizer.normalize(gameName)
+ val roundString = if (round > 9) round.toString() else "0$round"
+ svgBucket.upload(
+ path = "$gameString($gameId)/$roundString.svg",
+ data = svg.toByteArray(),
+ ) {
+ contentType = ContentType.Image.SVG
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/DesertDatabaseEntity.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/DesertDatabaseEntity.kt
new file mode 100644
index 0000000..8849495
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/DesertDatabaseEntity.kt
@@ -0,0 +1,79 @@
+package hu.mokegyesulet.it.dunestrat.backend.entities
+
+import hu.mokegyesulet.it.dunestrat.model.Desert
+import hu.mokegyesulet.it.dunestrat.model.DesertField
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+@Serializable
+class DesertDatabaseEntity(
+ @EncodeDefault(EncodeDefault.Mode.NEVER)
+ val id: Int = -1,
+ val name: String,
+ @SerialName("field_jsons")
+ val fieldJsons: List,
+) {
+ fun toDesert(): Desert {
+ val fieldsWithIdList = fieldJsons.map { Json.decodeFromString(it) }
+
+ val fields = mutableMapOf()
+ fieldsWithIdList.forEach { fieldWithIdList ->
+ fields[fieldWithIdList.id] = fieldWithIdList.toDesertField()
+ }
+
+ fieldsWithIdList.forEach { fieldWithIdList ->
+ val field = fields[fieldWithIdList.id]!!
+ fieldWithIdList.neighboursId.forEach { neighbourId ->
+ val neighbourField = fields[neighbourId]!!
+ field.neighbours.add(neighbourField)
+ }
+ }
+
+ return Desert(
+ id = id,
+ name = name,
+ fields = fields.values.toSet(),
+ )
+ }
+
+ companion object {
+ const val TABLE_NAME = "desert"
+ }
+}
+
+fun Desert.toDatabaseEntity() = DesertDatabaseEntity(
+ id = id,
+ name = name,
+ fieldJsons = fields.map { Json.encodeToString(it.toDesertFieldWithIdList()) },
+)
+
+@Serializable
+class DesertFieldWithIdList(
+ val id: String,
+ val water: Int,
+ val spice: Int,
+ val effectiveWeapon: Weapon,
+ val neighboursId: List,
+ val startingField: Boolean,
+) {
+ fun toDesertField() = DesertField(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = effectiveWeapon,
+ neighbours = mutableSetOf(),
+ startingField = startingField,
+ )
+}
+
+fun DesertField.toDesertFieldWithIdList() = DesertFieldWithIdList(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = effectiveWeapon,
+ neighboursId = neighbours.map { it.id },
+ startingField = startingField,
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameDatabaseEntity.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameDatabaseEntity.kt
new file mode 100644
index 0000000..75c07e2
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameDatabaseEntity.kt
@@ -0,0 +1,34 @@
+package hu.mokegyesulet.it.dunestrat.backend.entities
+
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.model.GameProgress
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+@Serializable
+class GameDatabaseEntity(
+ @EncodeDefault(EncodeDefault.Mode.NEVER)
+ val id: Int = -1,
+ val progress: Int,
+ @SerialName("desert_id")
+ val desertId: Int,
+ val json: String,
+) {
+ fun toGame(): Game {
+ val game = Json.decodeFromString(json)
+ return game.copy(id = id, progress = GameProgress.entries[progress], desertId = desertId)
+ }
+
+ companion object {
+ const val TABLE_NAME = "game"
+ }
+}
+
+fun Game.toDatabaseEntity(): GameDatabaseEntity = GameDatabaseEntity(
+ id = id,
+ this.progress.ordinal,
+ json = Json.encodeToString(this),
+ desertId = desertId,
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameStateDatabaseEntity.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameStateDatabaseEntity.kt
new file mode 100644
index 0000000..1cb26fb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/GameStateDatabaseEntity.kt
@@ -0,0 +1,155 @@
+package hu.mokegyesulet.it.dunestrat.backend.entities
+
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.model.GameStateField
+import hu.mokegyesulet.it.dunestrat.model.Player
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+@Serializable
+class GameStateDatabaseEntity(
+ @EncodeDefault(EncodeDefault.Mode.NEVER)
+ val id: Int = -1,
+ @SerialName("game_id")
+ val gameId: Int,
+ val index: Int,
+ @SerialName("field_jsons")
+ val fieldJsons: List,
+ @SerialName("player_jsons")
+ val playersJsons: List,
+) {
+ fun toGameState(): GameState {
+ val fieldsWithIdList = fieldJsons.map { fieldJson ->
+ Json.decodeFromString(fieldJson)
+ }
+
+ val fields = mutableMapOf()
+
+ fieldsWithIdList.forEach { fieldWithIdList ->
+ fields[fieldWithIdList.id] = fieldWithIdList.toGameStateField()
+ }
+
+ fieldsWithIdList.forEach { fieldWithIdList ->
+ val field = fields[fieldWithIdList.id]!!
+ fieldWithIdList.neighboursId.forEach { neighbourId ->
+ val neighbourField = fields[neighbourId]!!
+ field.neighbours.add(neighbourField)
+ }
+ }
+
+ val players = playersJsons.map { playerJson ->
+ val playerWithFieldIdList = Json.decodeFromString(playerJson)
+ val ownedFields = playerWithFieldIdList.ownedFieldsId.map { fieldId ->
+ fields[fieldId]!!
+ }
+ Player(
+ id = playerWithFieldIdList.id,
+ water = playerWithFieldIdList.water,
+ spice = playerWithFieldIdList.spice,
+ harvestersPurchased = playerWithFieldIdList.harvesterPurchased,
+ weapons = playerWithFieldIdList.weapons.toMutableMap(),
+ ownedFields = ownedFields.toMutableSet(),
+ )
+ }
+
+ return GameState(
+ id = id,
+ gameId = gameId,
+ index = index,
+ fields = fields.values.toSet(),
+ players = players.toSet(),
+ )
+ }
+
+ companion object {
+ const val TABLE_NAME = "game_state"
+ }
+}
+
+fun GameState.toDatabaseEntity(): GameStateDatabaseEntity = GameStateDatabaseEntity(
+ id = id,
+ gameId = gameId,
+ index = index,
+ fieldJsons = fields.map { Json.encodeToString(it.toGameStateFieldWithIdList()) },
+ playersJsons = players.map { Json.encodeToString(it.toPlayerWithFieldIdList()) },
+)
+
+@Serializable
+class GameStateFieldWithIdList(
+ val id: String,
+ val water: Int,
+ val spice: Int,
+ val effectiveWeapon: Weapon,
+ val harvester: Boolean,
+ val neighboursId: List,
+) {
+ fun toGameStateField() = GameStateField(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = effectiveWeapon,
+ harvester = harvester,
+ neighbours = mutableSetOf(),
+ )
+}
+
+fun GameStateField.toGameStateFieldWithIdList() = GameStateFieldWithIdList(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = effectiveWeapon,
+ harvester = harvester,
+ neighboursId = neighbours.map { it.id },
+)
+
+@Serializable
+class PlayerWithFieldIdList(
+ val id: Int,
+ val water: Int,
+ val spice: Int,
+ val harvesterPurchased: Int,
+ val weapons: Map,
+ val ownedFieldsId: Set,
+) {
+ fun toPlayer() = Player(
+ id = id,
+ water = water,
+ spice = spice,
+ harvestersPurchased = harvesterPurchased,
+ weapons = weapons.toMutableMap(),
+ ownedFields = mutableSetOf(),
+ )
+}
+
+fun Player.toPlayerWithFieldIdList() = PlayerWithFieldIdList(
+ id = id,
+ water = water,
+ spice = spice,
+ harvesterPurchased = harvestersPurchased,
+ weapons = weapons.toMap(),
+ ownedFieldsId = ownedFields.map { it.id }.toSet(),
+)
+
+@Serializable
+class GameStateSaveDatabaseEntity(
+ @SerialName("game_id")
+ val gameId: Int,
+ val index: Int,
+ @SerialName("field_jsons")
+ val fieldJsons: List,
+ @SerialName("player_jsons")
+ val playersJsons: List,
+ @SerialName("player_ids")
+ val playersIds: List,
+) {
+ constructor(gameStateDatabaseEntity: GameStateDatabaseEntity, playersIds: List) : this(
+ gameId = gameStateDatabaseEntity.gameId,
+ index = gameStateDatabaseEntity.index,
+ fieldJsons = gameStateDatabaseEntity.fieldJsons,
+ playersJsons = gameStateDatabaseEntity.playersJsons,
+ playersIds = playersIds,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/PlayerStepDatabaseEntity.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/PlayerStepDatabaseEntity.kt
new file mode 100644
index 0000000..4c6d990
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/backend/entities/PlayerStepDatabaseEntity.kt
@@ -0,0 +1,35 @@
+package hu.mokegyesulet.it.dunestrat.backend.entities
+
+import hu.mokegyesulet.it.dunestrat.model.PlayerStep
+import kotlinx.serialization.EncodeDefault
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+
+@Serializable
+class PlayerStepDatabaseEntity(
+ @EncodeDefault(EncodeDefault.Mode.NEVER)
+ val id: Int = -1,
+ @SerialName("game_state_id")
+ val gameStateId: Int,
+ @SerialName("player_id")
+ val playerId: Int,
+ val json: String,
+) {
+
+ fun toPlayerStep(): PlayerStep {
+ val playerStep = Json.decodeFromString(json)
+ return playerStep.copy(id = id, gameStateId = gameStateId, playerId = playerId)
+ }
+
+ companion object {
+ const val TABLE_NAME = "player_step"
+ }
+}
+
+fun PlayerStep.toDatabaseEntity(): PlayerStepDatabaseEntity = PlayerStepDatabaseEntity(
+ id = id,
+ gameStateId = gameStateId,
+ playerId = playerId,
+ json = Json.encodeToString(this),
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitScreen.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitScreen.kt
new file mode 100644
index 0000000..1493252
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitScreen.kt
@@ -0,0 +1,280 @@
+package hu.mokegyesulet.it.dunestrat.feature.init
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.*
+import androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import hu.mokegyesulet.it.dunestrat.model.Desert
+import hu.mokegyesulet.it.dunestrat.ui.GlobalStartingConditionsCard
+import hu.mokegyesulet.it.dunestrat.ui.PlayerStartingFieldCard
+import hu.mokegyesulet.it.dunestrat.ui.TeamConfigurationCard
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InitScreen(onNavigateBack: () -> Unit) {
+ val viewModel: InitViewModel = viewModel { InitViewModel() }
+ val phase by viewModel.currentPhase
+ val expanded by viewModel.dropdownExpanded
+ val playerCount by viewModel.playerCount
+ val mapOptions: List by viewModel.mapOptions.collectAsStateWithLifecycle()
+ val startingFieldIds by viewModel.startingFieldIds
+ val teams by viewModel.teams
+ val isFormValid by viewModel.isFormValid
+ val selectedDesert by viewModel.selectedDesert
+ val basePlayer by viewModel.basePlayerState
+ val gameName by viewModel.gameName
+
+ LaunchedEffect(Unit) {
+ viewModel.navigationEvent.collect {
+ onNavigateBack()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Új játék indítása") },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Vissza")
+ }
+ },
+ )
+ },
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(top = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (phase == InitializationPhase.STARTING_CONDITIONS) {
+ // Dropdown
+ Column(
+ modifier = Modifier.fillMaxWidth(0.7f),
+ ) {
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.ChangeMapDropdownExpanded(!expanded),
+ )
+ },
+
+ ) {
+ OutlinedTextField(
+ modifier = Modifier.menuAnchor(
+ ExposedDropdownMenuAnchorType.PrimaryNotEditable,
+ true,
+ ).fillMaxWidth(),
+ value = selectedDesert?.name?.ifBlank {
+ "${selectedDesert?.fields?.count {
+ it.startingField
+ }} játékos térkép"
+ } ?: "",
+ onValueChange = {},
+ readOnly = true,
+ label = { Text("Térkép kiválasztása") },
+ trailingIcon = { TrailingIcon(expanded = expanded) },
+ isError = selectedDesert == null,
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.ChangeMapDropdownExpanded(false),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(0.7f),
+ ) {
+ mapOptions.forEach { map ->
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = map.name.ifBlank {
+ "${map.fields.count {
+ it.startingField
+ }} játékos térkép"
+ },
+ )
+ },
+ onClick = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.ChangeSelectedMap(map),
+ )
+ },
+ )
+ }
+ }
+ }
+ if (selectedDesert == null) {
+ Text(
+ text = "A térkép kiválasztása kötelező!",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp, top = 6.dp),
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+ }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (phase == InitializationPhase.STARTING_CONDITIONS) {
+ if (selectedDesert != null) {
+ item {
+ Column(modifier = Modifier.padding(16.dp)) {
+ TextField(
+ value = gameName,
+ onValueChange = { input ->
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.ChangeGameName(input),
+ )
+ },
+ label = { Text("Játék neve") },
+ isError = gameName.trim() == "",
+ )
+ if (gameName.trim() == "") {
+ Text(
+ text = "A játék neve nem lehet üres!",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp, top = 6.dp),
+ )
+ }
+ }
+ }
+
+ item {
+ GlobalStartingConditionsCard(
+ modifier = Modifier.fillMaxWidth(0.9f),
+ player = basePlayer,
+ onChange = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent
+ .UpdateGlobalStartingConditions(
+ it,
+ ),
+ )
+ },
+ )
+ }
+
+ items(playerCount) { index ->
+ val currentFieldId = startingFieldIds[index]
+ val isDuplicate =
+ startingFieldIds.count { it == currentFieldId && it.isNotBlank() } >
+ 1
+ val availableFields =
+ selectedDesert?.fields?.filter { it.startingField }?.map { it.id }
+ ?: emptyList()
+
+ PlayerStartingFieldCard(
+ index = index,
+ modifier = Modifier.fillMaxWidth(0.45f),
+ startingFieldId = currentFieldId,
+ onStartingFieldChange = { fieldId ->
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.UpdatePlayerStartingField(
+ fieldId,
+ index,
+ ),
+ )
+ },
+ availableStartingFields = availableFields,
+ isFieldDuplicate = isDuplicate,
+ )
+ }
+
+ item {
+ Button(
+ onClick = {
+ viewModel.onEvent(InitViewModel.InitScreenEvent.ProceedToTeams)
+ },
+ enabled = isFormValid,
+ modifier = Modifier.padding(16.dp),
+ ) {
+ Text("Tovább a csapatokhoz")
+ }
+ }
+ }
+ } else {
+ item {
+ Text(
+ text = "Csapatok beállítása",
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier.padding(16.dp),
+ )
+ }
+
+ items(teams.size) { teamIndex ->
+ TeamConfigurationCard(
+ teamIndex = teamIndex,
+ team = teams[teamIndex],
+ onAddStudent = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.AddStudent(teamIndex),
+ )
+ },
+ onRemoveStudent = { studentIndex ->
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.RemoveStudent(
+ teamIndex,
+ studentIndex,
+ ),
+ )
+ },
+ onUpdateStudent = { studentIndex, student ->
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.UpdateStudent(
+ teamIndex,
+ studentIndex,
+ student,
+ ),
+ )
+ },
+ modifier = Modifier.fillMaxWidth(0.9f),
+ )
+ }
+
+ item {
+ Row(modifier = Modifier.padding(16.dp)) {
+ OutlinedButton(
+ onClick = {
+ viewModel.onEvent(
+ InitViewModel.InitScreenEvent.BackToConditions,
+ )
+ },
+ modifier = Modifier.padding(end = 8.dp),
+ ) {
+ Text("Vissza")
+ }
+ Button(
+ onClick = { viewModel.savePlayers() },
+ enabled = isFormValid,
+ ) {
+ Text("Játék indítása")
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitViewModel.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitViewModel.kt
new file mode 100644
index 0000000..6c4f021
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/init/InitViewModel.kt
@@ -0,0 +1,272 @@
+package hu.mokegyesulet.it.dunestrat.feature.init
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseRepository
+import hu.mokegyesulet.it.dunestrat.model.*
+import hu.mokegyesulet.it.dunestrat.util.drawmap.MapDrawer
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+enum class InitializationPhase {
+ STARTING_CONDITIONS,
+ TEAMS,
+}
+
+class InitViewModel() : ViewModel() {
+
+ private fun createDefaultPlayer(index: Int) = Player(
+ id = index,
+ water = 0,
+ spice = 10,
+ weapons = mutableMapOf(
+ Weapon.PISTOL to 0,
+ Weapon.LASGUN to 0,
+ Weapon.CRYSKNIFE to 0,
+ Weapon.LEGION to 0,
+ ),
+ ownedFields = mutableSetOf(),
+ harvestersPurchased = 0,
+ )
+
+ val currentPhase = mutableStateOf(InitializationPhase.STARTING_CONDITIONS)
+ val basePlayerState: MutableState = mutableStateOf(createDefaultPlayer(-1))
+ val playerList: MutableState>
+ val startingFieldIds: MutableState>
+ val teams: MutableState> = mutableStateOf(emptyList())
+ val gameName = mutableStateOf("")
+
+ private val _navigationEvent = MutableSharedFlow()
+ val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow()
+
+ init {
+ val startingPlayers = mutableListOf()
+ val emptyFieldIds = mutableListOf()
+ repeat(12) { index ->
+ startingPlayers.add(createDefaultPlayer(index))
+ emptyFieldIds.add("")
+ }
+ playerList = mutableStateOf(startingPlayers)
+ startingFieldIds = mutableStateOf(emptyFieldIds)
+ }
+
+ var dropdownExpanded = mutableStateOf(false)
+ val playerCount = mutableStateOf(12)
+ val mapOptions = SupabaseRepository.getDeserts()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+ val isFormValid = mutableStateOf(false)
+
+ val selectedDesert: MutableState = mutableStateOf(null)
+
+ fun onEvent(event: InitScreenEvent) {
+ when (event) {
+ is InitScreenEvent.ChangeMapDropdownExpanded -> {
+ dropdownExpanded.value = event.expanded
+ }
+
+ is InitScreenEvent.ChangeGameName -> {
+ gameName.value = event.name
+ validateForm()
+ }
+
+ is InitScreenEvent.ChangeSelectedMap -> {
+ selectedDesert.value = event.selectedDesert
+
+ val startingFields = event.selectedDesert.fields.filter { it.startingField }
+ playerCount.value = startingFields.size
+ dropdownExpanded.value = false
+ resizePlayerList(playerCount.value)
+
+ startingFieldIds.value = startingFields.map { it.id }
+ validateForm()
+ }
+
+ is InitScreenEvent.UpdateGlobalStartingConditions -> {
+ basePlayerState.value = event.playerData
+ validateForm()
+ }
+
+ is InitScreenEvent.UpdatePlayerStartingField -> {
+ val newStartingFieldIds = startingFieldIds.value.toMutableList()
+ newStartingFieldIds[event.index] = event.fieldId
+ startingFieldIds.value = newStartingFieldIds
+ validateForm()
+ }
+
+ is InitScreenEvent.ProceedToTeams -> {
+ if (teams.value.isEmpty() || teams.value.size != playerList.value.size) {
+ teams.value = playerList.value.map { player ->
+ Team(player.id, listOf(Student("")))
+ }
+ }
+ currentPhase.value = InitializationPhase.TEAMS
+ validateForm()
+ }
+
+ is InitScreenEvent.BackToConditions -> {
+ currentPhase.value = InitializationPhase.STARTING_CONDITIONS
+ validateForm()
+ }
+
+ is InitScreenEvent.AddStudent -> {
+ val newTeams = teams.value.toMutableList()
+ val team = newTeams[event.teamIndex]
+ val newStudents = team.students.toMutableList()
+ newStudents.add(Student(""))
+ newTeams[event.teamIndex] = Team(team.playerId, newStudents)
+ teams.value = newTeams
+ validateForm()
+ }
+
+ is InitScreenEvent.RemoveStudent -> {
+ val newTeams = teams.value.toMutableList()
+ val team = newTeams[event.teamIndex]
+ if (team.students.size > 1) {
+ val newStudents = team.students.toMutableList()
+ newStudents.removeAt(event.studentIndex)
+ newTeams[event.teamIndex] = Team(team.playerId, newStudents)
+ teams.value = newTeams
+ validateForm()
+ }
+ }
+
+ is InitScreenEvent.UpdateStudent -> {
+ val newTeams = teams.value.toMutableList()
+ val team = newTeams[event.teamIndex]
+ val newStudents = team.students.toMutableList()
+ newStudents[event.studentIndex] = event.student
+ newTeams[event.teamIndex] = Team(team.playerId, newStudents)
+ teams.value = newTeams
+ validateForm()
+ }
+
+ is InitScreenEvent.InitPlayerOnMap -> {}
+
+ is InitScreenEvent.UpdatePlayerData -> {}
+ }
+ }
+
+ private fun validateForm() {
+ if (currentPhase.value == InitializationPhase.STARTING_CONDITIONS) {
+ val desertSelected = selectedDesert.value != null
+ val allFieldsAssigned = startingFieldIds.value.all { it.isNotBlank() }
+ val uniqueFields = startingFieldIds.value.distinct().size == startingFieldIds.value.size
+ val named = gameName.value.trim() != ""
+
+ val basePlayer = basePlayerState.value
+ val baseConditionsValid = basePlayer.water >= 0 &&
+ basePlayer.spice >= 0 &&
+ Weapon.entries.all { basePlayer.getWeaponCount(it) >= 0 }
+
+ isFormValid.value =
+ desertSelected &&
+ allFieldsAssigned &&
+ uniqueFields &&
+ named &&
+ baseConditionsValid
+ } else {
+ isFormValid.value = teams.value.all { team ->
+ team.students.all { student -> student.name.isNotBlank() }
+ }
+ }
+ }
+
+ private fun resizePlayerList(newCount: Int) {
+ val currentList = playerList.value.toMutableList()
+ val currentFields = startingFieldIds.value.toMutableList()
+
+ if (currentList.size > newCount) {
+ playerList.value = currentList.take(newCount)
+ startingFieldIds.value = currentFields.take(newCount)
+ } else {
+ repeat(newCount - currentList.size) { offset ->
+ playerList.value += createDefaultPlayer(currentList.size + offset)
+ startingFieldIds.value += ""
+ }
+ }
+ playerCount.value = newCount
+ teams.value = emptyList() // Reset teams when count changes
+ validateForm()
+ }
+
+ fun savePlayers() {
+ val desert = selectedDesert.value ?: return
+ val base = basePlayerState.value
+ val finalTeams = teams.value
+
+ viewModelScope.launch {
+ println("Játékosok mentése...")
+
+ val gameTemplate = Game(
+ name = gameName.value,
+ progress = GameProgress.INITIALIZED,
+ teams = finalTeams,
+ desertId = desert.id,
+ )
+ val savedGame = SupabaseRepository.saveGame(gameTemplate)
+
+ val stateFields = desert.fields.toGameStateFields()
+
+ val initialPlayers = playerList.value.mapIndexed { index, player ->
+ val fieldId = startingFieldIds.value[index]
+ player.copy(
+ water = base.water,
+ spice = base.spice,
+ weapons = Weapon.entries.associateWith {
+ base.getWeaponCount(it)
+ }.toMutableMap(),
+ ownedFields = mutableSetOf(stateFields.first { it.id == fieldId }),
+ )
+ }
+
+ val firstState = GameState(
+ id = -1,
+ gameId = savedGame.id,
+ index = 0,
+ fields = stateFields,
+ players = initialPlayers.toSet(),
+ )
+ SupabaseRepository.saveGameState(firstState)
+ MapDrawer.loadResources()
+ val svg = MapDrawer.getGameStateSvg(firstState)
+ SupabaseRepository.uploadImage(
+ svg,
+ savedGame.name,
+ savedGame.id.toString(),
+ 0,
+ )
+
+ println("Játék és kezdeti állapot sikeresen mentve! Game ID: ${savedGame.id}")
+ _navigationEvent.emit(Unit)
+ }
+ }
+
+ sealed class InitScreenEvent {
+ data object InitPlayerOnMap : InitScreenEvent()
+ data class ChangeMapDropdownExpanded(val expanded: Boolean) : InitScreenEvent()
+ data class ChangeGameName(val name: String) : InitScreenEvent()
+ data class ChangeSelectedMap(val selectedDesert: Desert) : InitScreenEvent()
+ data class UpdateGlobalStartingConditions(val playerData: Player) : InitScreenEvent()
+ data class UpdatePlayerData(val playerData: Player, val index: Int) : InitScreenEvent()
+ data class UpdatePlayerStartingField(
+ val fieldId: String,
+ val index: Int,
+ ) : InitScreenEvent()
+
+ data object ProceedToTeams : InitScreenEvent()
+ data object BackToConditions : InitScreenEvent()
+ data class AddStudent(val teamIndex: Int) : InitScreenEvent()
+ data class RemoveStudent(val teamIndex: Int, val studentIndex: Int) : InitScreenEvent()
+ data class UpdateStudent(
+ val teamIndex: Int,
+ val studentIndex: Int,
+ val student: Student,
+ ) : InitScreenEvent()
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/Inventory.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/Inventory.kt
new file mode 100644
index 0000000..7dda574
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/Inventory.kt
@@ -0,0 +1,128 @@
+package hu.mokegyesulet.it.dunestrat.feature.inventory
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import hu.mokegyesulet.it.dunestrat.model.Player
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import hu.mokegyesulet.it.dunestrat.util.openURL
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun InventoryScreen(
+ gameId: Int,
+ onBack: () -> Unit,
+) {
+ val viewModel: InventoryViewModel = viewModel { InventoryViewModel(gameId) }
+ val players by viewModel.players
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(text = "Nyersanyagok - Játék: $gameId") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Vissza")
+ }
+ },
+ actions = {
+ Button(
+ onClick = {
+ openURL(viewModel.svgDownloadUrl!!)
+ },
+ ) {
+ Text(text = "Térkép letöltése")
+ }
+ },
+ )
+ },
+ ) { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(16.dp),
+ ) {
+ Box(
+ modifier = Modifier.fillMaxWidth(),
+ contentAlignment = Alignment.TopStart,
+ ) {
+ Column(modifier = Modifier.widthIn(max = 700.dp)) {
+ Row(
+ modifier = Modifier.padding(start = 12.dp, end = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = "Sorszám", modifier = Modifier.width(80.dp))
+ Text(text = "Víz", modifier = Modifier.width(64.dp))
+ Text(text = "Fűszer", modifier = Modifier.width(64.dp))
+ Text(text = "LSG", modifier = Modifier.width(64.dp))
+ Text(text = "MPT", modifier = Modifier.width(64.dp))
+ Text(text = "CRK", modifier = Modifier.width(64.dp))
+ Text(text = "Légió", modifier = Modifier.width(72.dp))
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ LazyColumn(modifier = Modifier.fillMaxHeight()) {
+ items(players) { player ->
+ val alternateBackground = players.indexOf(player) % 2 == 0
+ InventoryRow(
+ player = player,
+ alternateBackground = alternateBackground,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun InventoryRow(
+ player: Player,
+ alternateBackground: Boolean,
+) {
+ val lsg = player.getWeaponCount(Weapon.LASGUN).toString()
+ val mpt = player.getWeaponCount(Weapon.PISTOL).toString()
+ val crk = player.getWeaponCount(Weapon.CRYSKNIFE).toString()
+ val legion = player.getWeaponCount(Weapon.LEGION).toString()
+
+ val rowContent: @Composable () -> Unit = {
+ Row(
+ modifier = Modifier
+ .padding(start = 12.dp, top = 8.dp, bottom = 8.dp, end = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(text = player.id.toString(), modifier = Modifier.width(80.dp))
+ Text(text = player.water.toString(), modifier = Modifier.width(64.dp))
+ Text(text = player.spice.toString(), modifier = Modifier.width(64.dp))
+ Text(text = lsg, modifier = Modifier.width(64.dp))
+ Text(text = mpt, modifier = Modifier.width(64.dp))
+ Text(text = crk, modifier = Modifier.width(64.dp))
+ Text(text = legion, modifier = Modifier.width(72.dp))
+ }
+ }
+
+ if (alternateBackground) {
+ Surface(
+ color = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
+ ) {
+ rowContent()
+ }
+ } else {
+ rowContent()
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/InventoryViewModel.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/InventoryViewModel.kt
new file mode 100644
index 0000000..c679a4b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/inventory/InventoryViewModel.kt
@@ -0,0 +1,47 @@
+package hu.mokegyesulet.it.dunestrat.feature.inventory
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseRepository
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.model.Player
+import hu.mokegyesulet.it.dunestrat.util.StringNormalizer
+import kotlinx.coroutines.launch
+
+class InventoryViewModel(
+ val gameId: Int,
+) : ViewModel() {
+
+ val gameState: MutableState = mutableStateOf(null)
+ val players = mutableStateOf>(emptyList())
+
+ private lateinit var normalizedGameName: String
+
+ init {
+ viewModelScope.launch {
+ try {
+ val game = SupabaseRepository.getGameById(gameId)
+ normalizedGameName = StringNormalizer.normalize(game.name)
+ val latest = SupabaseRepository.getLatestGameStateByGameId(gameId)
+ gameState.value = latest
+ players.value = latest.players.toList().sortedBy { it.id }
+ } catch (e: Exception) {
+ println("HIBA: nem sikerült betölteni a játék állapotát: ${e.message}")
+ players.value = emptyList()
+ }
+ }
+ }
+
+ val svgDownloadUrl: String?
+ get() {
+ return if (gameState.value == null) {
+ null
+ } else {
+ val round = gameState.value!!.index
+ val roundString = if (round > 9) round.toString() else "0$round"
+ "https://lnwvuwepwaexwybselsf.supabase.co/storage/v1/object/public/svgs/$normalizedGameName($gameId)/$roundString.svg"
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenu.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenu.kt
new file mode 100644
index 0000000..4aebe22
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenu.kt
@@ -0,0 +1,155 @@
+package hu.mokegyesulet.it.dunestrat.feature.mainmenu
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.ui.typography
+import hu.mokegyesulet.it.dunestrat.util.openURL
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainMenu(
+ onInputMovesClick: (Int) -> Unit,
+ onStat: (Int) -> Unit,
+ onNewGameClick: () -> Unit,
+ onInventoryClick: (Int) -> Unit,
+) {
+ val viewModel = viewModel { MainMenuViewModel() }
+
+ val loggedIn by viewModel.isLoggedIn.collectAsState(initial = false)
+
+ Scaffold(
+ topBar = {
+ TopAppBar(title = { Text(if (loggedIn) "Dűne - Főmenü" else "Dűne - Bejelentkezés") })
+ },
+ ) { paddingValues ->
+ if (loggedIn) {
+ val games by viewModel.games.collectAsStateWithLifecycle()
+ val selectedGame by viewModel.selectedGame
+ val latestGameState by viewModel.latestGameState
+ val players by viewModel.players
+
+ Row(
+ modifier = Modifier.fillMaxSize().padding(paddingValues).padding(50.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Column(modifier = Modifier.width(250.dp).fillMaxHeight()) {
+ ExtendedFloatingActionButton(
+ onClick = onNewGameClick,
+ icon = { Icon(Icons.Filled.Add, contentDescription = "Add new game") },
+ text = { Text(text = "Új játék") },
+ modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
+ )
+
+ LazyColumn(modifier = Modifier.weight(1f)) {
+ items(games) { game: Game ->
+ NavigationDrawerItem(
+ label = { Text(text = game.name) },
+ selected = selectedGame == game,
+ onClick = {
+ viewModel.onEvent(MainMenuViewModel.Event.SelectGame(game))
+ },
+ modifier = Modifier.padding(vertical = 4.dp),
+ )
+ }
+ }
+ }
+ Column(
+ modifier = Modifier.fillMaxHeight(),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ Button(
+ onClick = {
+ onInputMovesClick(selectedGame?.id ?: -1)
+ viewModel.onEvent(MainMenuViewModel.Event.UnSelectGame)
+ },
+ enabled = selectedGame != null,
+ ) {
+ Text(text = "Lépések bevitele")
+ }
+ Button(
+ onClick = {
+ viewModel.svgDownloadUrl?.let { openURL(it) }
+ },
+ enabled = selectedGame != null && viewModel.svgDownloadUrl != null,
+ ) {
+ Text(text = "Térkép letöltése")
+ }
+ Button(
+ onClick = { selectedGame?.let { onInventoryClick(it.id) } },
+ enabled = selectedGame != null,
+ ) {
+ Text(text = "Csapatok nyersanyagai")
+ }
+ if (selectedGame != null) {
+ if (latestGameState == null) {
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.weight(1f)
+ .wrapContentHeight(),
+ ) {
+ item {
+ Text(
+ text = "${latestGameState?.index ?: -1 }. Kör",
+ style = typography.titleSmall,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(vertical = 8.dp),
+ )
+ }
+ items(players.toList()) { player ->
+ val id = player.id
+ val water = player.water
+ val spice = player.spice
+ Text(
+ text = "$id: $water víz, $spice fűszer",
+ )
+ }
+ }
+ }
+ } else {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+// Button(
+// onClick = { onStat(selectedGame?.id ?: -1) },
+// enabled = selectedGame != null,
+// ) {
+// Text(text = "Statisztika")
+// }
+ }
+ }
+ } else {
+ Box(
+ modifier = Modifier.fillMaxSize().padding(paddingValues),
+ ) {
+ Button(
+ onClick = {
+ viewModel.onLogin()
+ },
+ modifier = Modifier.align(Alignment.Center),
+ ) {
+ Text(text = "Bejelentkezés")
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenuViewModel.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenuViewModel.kt
new file mode 100644
index 0000000..1bbf56a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/mainmenu/MainMenuViewModel.kt
@@ -0,0 +1,89 @@
+package hu.mokegyesulet.it.dunestrat.feature.mainmenu
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import hu.mokegyesulet.it.dunestrat.backend.AuthStatus
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseAuth
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseRepository
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.util.StringNormalizer.normalize
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class MainMenuViewModel : ViewModel() {
+
+ fun onLogin() {
+ viewModelScope.launch {
+ SupabaseAuth.logIn()
+ }
+ }
+
+ fun onLogout() {
+ viewModelScope.launch {
+ SupabaseAuth.logOut()
+ }
+ }
+
+ val isLoggedIn: Flow
+ get() = SupabaseAuth.authStatus.map {
+ it is AuthStatus.Authenticated
+ }
+
+ val games = SupabaseRepository.getGames().stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ emptyList(),
+ )
+
+ val selectedGame = mutableStateOf(null)
+
+ val latestGameState = mutableStateOf(null)
+
+ val players = derivedStateOf {
+ latestGameState.value?.players ?: emptySet()
+ }
+
+ val svgDownloadUrl: String?
+ get() {
+ return if (latestGameState.value == null) {
+ null
+ } else {
+ val name = normalize(selectedGame.value!!.name)
+ val round = latestGameState.value!!.index
+ val roundString = if (round > 9) round.toString() else "0$round"
+ "https://lnwvuwepwaexwybselsf.supabase.co/storage/v1/object/public/svgs/$name(${selectedGame.value!!.id})/$roundString.svg"
+ }
+ }
+ sealed class Event {
+ data class SelectGame(val game: Game) : Event()
+ data object UnSelectGame : Event()
+ }
+ fun onEvent(event: Event) {
+ when (event) {
+ is Event.SelectGame -> {
+ if (selectedGame.value != event.game) {
+ selectedGame.value = event.game
+ latestGameState.value = null
+ viewModelScope.launch {
+ latestGameState.value =
+ SupabaseRepository.getLatestGameStateByGameId(event.game.id)
+ }
+ } else {
+ selectedGame.value = null
+ latestGameState.value = null
+ }
+ }
+
+ Event.UnSelectGame -> {
+ selectedGame.value = null
+ latestGameState.value = null
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputScreen.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputScreen.kt
new file mode 100644
index 0000000..92977cf
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputScreen.kt
@@ -0,0 +1,329 @@
+package hu.mokegyesulet.it.dunestrat.feature.playerstep
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material.icons.filled.QuestionMark
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.viewmodel.compose.viewModel
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import hu.mokegyesulet.it.dunestrat.ui.tabKeyNavigable
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PlayerStepInputScreen(
+ gameId: Int,
+ onBack: () -> Unit,
+ onRunTurn: () -> Unit,
+) {
+ val viewModel = viewModel { PlayerStepInputViewModel(gameId) }
+ val tabIndex by viewModel.tabIndex
+ val isRunningTurn by viewModel.isRunningTurn
+ val members by viewModel.members
+
+ val game by viewModel.game
+ val gameState by viewModel.gameState
+
+ if (game == null || gameState == null) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(text = "Loading...")
+ }
+ } else if (isRunningTurn) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator()
+ }
+ } else {
+ val playerIds = game!!.teams.map { it.playerId }
+
+ val leaveFields: List>
+ by viewModel.leaveFields
+
+ val enterFields: List>
+ by viewModel.enterFields
+
+ val purchaseWeapons: Map by viewModel.purchaseWeapons
+
+ val uiState by viewModel.uiState
+ val purchaseHarvester by uiState.purchaseHarvester
+
+ val saveOnLostFocus: (FocusState) -> Unit = {
+ if (!it.isFocused) {
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.SaveToDatabase,
+ )
+ }
+ }
+
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner) {
+ onDispose { viewModel.onEvent(PlayerStepInputViewModel.Event.SaveToDatabase) }
+ }
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ TopAppBar(
+ title = { Text("Lépesek bevitele") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Vissza")
+ }
+ },
+ actions = {
+ Button(
+ modifier = Modifier.padding(horizontal = 4.dp),
+ onClick = {
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.SaveToDatabase,
+ )
+ },
+ ) {
+ Text(text = "Mentés")
+ }
+
+ Button(
+ modifier = Modifier.padding(horizontal = 4.dp),
+ onClick = {
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.RunTurn {
+ onRunTurn()
+ },
+ )
+ },
+ ) {
+ Text(text = "Kör futtatása")
+ }
+ },
+ )
+ },
+ bottomBar = {
+ PrimaryScrollableTabRow(
+ selectedTabIndex = tabIndex,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ playerIds.forEachIndexed { index, playerId ->
+ Tab(
+ selected = index == tabIndex,
+ onClick = {
+ viewModel.onEvent(PlayerStepInputViewModel.Event.TabSelected(index))
+ },
+ text = {
+ Text(text = playerId.toString())
+ },
+ )
+ }
+ }
+ },
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier.padding(paddingValues).fillMaxSize(),
+ ) {
+ Column {
+ Row {
+ FieldInputColumn(
+ title = "Lelépés",
+ fields = leaveFields,
+ onValueChange = { index, value ->
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.LeaveField(index, value),
+ )
+ },
+ onFocusChanged = saveOnLostFocus,
+ modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
+ )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
+ ) {
+ Text("Vásárlás")
+
+ val focusManager = LocalFocusManager.current
+
+ Weapon.entries.forEach { weapon ->
+ TextField(
+ label = {
+ Text(
+ text = when (weapon) {
+ Weapon.PISTOL -> "MPT"
+ Weapon.LASGUN -> "LSG"
+ Weapon.CRYSKNIFE -> "CRK"
+ Weapon.LEGION -> "Légió"
+ },
+
+ )
+ },
+ value = if ((purchaseWeapons[weapon] ?: 0) != 0) {
+ (purchaseWeapons[weapon] ?: 0).toString()
+ } else {
+ ""
+ },
+ placeholder = {
+ Text(text = "0")
+ },
+ onValueChange = {
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.PurchaseWeapon(
+ weapon = weapon,
+ value = it,
+ ),
+ )
+ },
+ modifier = Modifier.onFocusChanged(
+ saveOnLostFocus,
+ ).tabKeyNavigable(focusManager)
+ .padding(horizontal = 4.dp, vertical = 6.dp),
+ )
+ }
+ FieldRow(
+ value = purchaseHarvester.first,
+ label = "Harvester",
+ onValueChange = {
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.PurchaseHarvester(it),
+ )
+ },
+ validation = purchaseHarvester.second,
+ modifier = Modifier.onFocusChanged(
+ saveOnLostFocus,
+ ).tabKeyNavigable(focusManager)
+ .padding(horizontal = 4.dp, vertical = 26.dp),
+ )
+ }
+ FieldInputColumn(
+ title = "Rálépés",
+ fields = enterFields,
+ onValueChange = { index, value ->
+ viewModel.onEvent(
+ PlayerStepInputViewModel.Event.EnterField(index, value),
+ )
+ },
+ onFocusChanged = saveOnLostFocus,
+ modifier = Modifier.weight(1f).padding(horizontal = 16.dp),
+ )
+ }
+
+ Spacer(Modifier.weight(1f))
+ Text("Csapattagok: $members")
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun FieldRow(
+ value: String,
+ onValueChange: (String) -> Unit,
+ validation: PlayerStepInputViewModel.Validation?,
+ modifier: Modifier = Modifier,
+ label: String? = null,
+) {
+ val focusManager = LocalFocusManager.current
+
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ TextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 8.dp)
+ .tabKeyNavigable(focusManager),
+ label = if (label != null) {
+ { Text(text = label) }
+ } else {
+ null
+ },
+ )
+ val tooltipText = validation?.message
+ val icon = @Composable {
+ Icon(
+ imageVector = when (validation?.isError) {
+ null -> Icons.Filled.Check
+ true -> Icons.Default.QuestionMark
+ false -> Icons.Default.Error
+ },
+ contentDescription = tooltipText,
+ tint = when (validation?.isError) {
+ null -> Color.Green
+ true -> Color.Red
+ false -> Color(1.0f, 0.5f, 0.0f)
+ },
+ )
+ }
+
+ if (tooltipText != null) {
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
+ positioning = TooltipAnchorPosition.Above,
+ ),
+ tooltip = {
+ PlainTooltip {
+ Text(text = tooltipText)
+ }
+ },
+ state = rememberTooltipState(),
+ ) {
+ icon()
+ }
+ } else {
+ icon()
+ }
+ }
+}
+
+@Composable
+private fun FieldInputColumn(
+ title: String,
+ fields: List>,
+ onValueChange: (Int, String) -> Unit,
+ onFocusChanged: (FocusState) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(text = title)
+ LazyColumn {
+ itemsIndexed(fields) {
+ index: Int,
+ fieldData: Pair,
+ ->
+ FieldRow(
+ value = fieldData.first,
+ onValueChange = {
+ onValueChange(index, it)
+ },
+ validation = fieldData.second,
+ modifier = Modifier.onFocusChanged(onFocusChanged),
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputViewModel.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputViewModel.kt
new file mode 100644
index 0000000..6ffa41c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/playerstep/PlayerStepInputViewModel.kt
@@ -0,0 +1,319 @@
+package hu.mokegyesulet.it.dunestrat.feature.playerstep
+
+import androidx.compose.runtime.*
+import androidx.compose.runtime.snapshots.SnapshotStateList
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.text.toUpperCase
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseRepository
+import hu.mokegyesulet.it.dunestrat.model.Game
+import hu.mokegyesulet.it.dunestrat.model.GameState
+import hu.mokegyesulet.it.dunestrat.model.PlayerStep
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import hu.mokegyesulet.it.dunestrat.util.drawmap.MapDrawer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+
+class PlayerStepInputViewModel(
+ val gameId: Int,
+) : ViewModel() {
+
+ val tabIndex = mutableStateOf(0)
+ val isRunningTurn = mutableStateOf(false)
+
+ val game = mutableStateOf(null)
+ val gameState = mutableStateOf(null)
+ val playerSteps = mutableStateOf(listOf())
+
+ val loaded = derivedStateOf {
+ game.value != null && gameState.value != null && playerSteps.value.isNotEmpty()
+ }
+
+ init {
+ viewModelScope.async {
+ game.value = SupabaseRepository.getGameById(gameId)
+ gameState.value = SupabaseRepository.getLatestGameStateByGameId(gameId)
+ playerSteps.value =
+ game.value!!.teams.map { team ->
+ SupabaseRepository.getPlayerStep(gameState.value!!.id, team.playerId)
+ }
+ }
+ }
+
+ // Expose members as a stored derived State (not a getter) so Compose tracks it properly
+ val members = derivedStateOf {
+ game.value?.teams?.getOrNull(tabIndex.value)?.students?.joinToString { it.name } ?: ""
+ }
+
+ val uiStates = derivedStateOf {
+ playerSteps.value.map { playerStep ->
+
+ val purchaseWeapons = mutableStateMapOf()
+ purchaseWeapons.putAll(playerStep.purchaseWeapons)
+
+ EnterStepsUIState(
+ stepId = playerStep.id,
+ playerId = playerStep.playerId,
+ leaveFields = (playerStep.leaveFields + "").map {
+ it to validateLeaveField(it)
+ }.toMutableStateList(),
+ enterFields = (playerStep.enterFields + "").map {
+ it to validateEnterField(it)
+ }.toMutableStateList(),
+ purchaseWeapons = purchaseWeapons,
+ purchaseHarvester = mutableStateOf(
+ (playerStep.buildHarvesters.firstOrNull() ?: "") to
+ validatePurchaseHarvester(playerStep.buildHarvesters.firstOrNull() ?: ""),
+ ),
+ )
+ }
+ }
+
+ // Expose uiState as a stored derived State so consumers observe changes to uiStates and tabIndex
+ val uiState = derivedStateOf {
+ uiStates.value.getOrNull(tabIndex.value) ?: EnterStepsUIState()
+ }
+
+ // Compose consumers often need to observe list mutations directly. Return the SnapshotStateList
+ // for the current tab so composables can read it and be subscribed to its changes.
+ val leaveFields = derivedStateOf {
+ uiStates.value.getOrNull(tabIndex.value)?.leaveFields ?: mutableStateListOf(Pair("", null))
+ }
+
+ val enterFields = derivedStateOf {
+ uiStates.value.getOrNull(tabIndex.value)?.enterFields ?: mutableStateListOf(Pair("", null))
+ }
+
+ val purchaseWeapons = derivedStateOf {
+ uiStates.value.getOrNull(tabIndex.value)?.purchaseWeapons ?: mutableStateMapOf()
+ }
+
+ fun onEvent(event: Event) {
+ if (!loaded.value) {
+ return
+ }
+ val gameState =
+ gameState.value ?: throw IllegalStateException("Game state is not loaded yet!")
+ when (event) {
+ is Event.TabSelected -> {
+ tabIndex.value = event.index
+ }
+
+ is Event.RunTurn -> {
+ isRunningTurn.value = true
+ CoroutineScope(Dispatchers.Unconfined).launch {
+ val playerSteps = getPlayerSteps()
+ val newGameState = gameState.runTurn(playerSteps)
+ SupabaseRepository.saveGameState(newGameState)
+ MapDrawer.loadResources()
+ val svg = MapDrawer.getGameStateSvg(newGameState)
+ SupabaseRepository.uploadImage(
+ svg = svg,
+ gameName = game.value!!.name,
+ gameId = game.value!!.id.toString(),
+ round = newGameState.index,
+ )
+ isRunningTurn.value = false
+ event.onComplete()
+ }
+ }
+
+ is Event.LeaveField -> {
+ val leaveFields = uiStates.value[tabIndex.value].leaveFields
+ val fieldId = event.value.toFieldId()
+ val validation = validateLeaveField(fieldId)
+ leaveFields[event.index] =
+ fieldId to validation
+ if (!leaveFields.contains("" to null)) {
+ leaveFields.add("" to null)
+ }
+ }
+
+ is Event.EnterField -> {
+ val enterFields = uiStates.value[tabIndex.value].enterFields
+ val fieldId = event.value.toFieldId()
+ val validation = validateEnterField(fieldId)
+ enterFields[event.index] =
+ fieldId to validation
+ if (!enterFields.contains("" to null)) {
+ enterFields.add("" to null)
+ }
+ }
+
+ is Event.PurchaseWeapon -> {
+ if (event.value == "") {
+ uiStates.value[tabIndex.value].purchaseWeapons[event.weapon] = 0
+ }
+ try {
+ val amount = event.value.toInt()
+ if (amount >= 0) {
+ uiStates.value[tabIndex.value].purchaseWeapons[event.weapon] = amount
+ }
+ } catch (_: NumberFormatException) {}
+ }
+
+ is Event.PurchaseHarvester -> {
+ val fieldId = event.value.toFieldId()
+ val validation = validatePurchaseHarvester(fieldId)
+
+ uiStates.value[tabIndex.value].purchaseHarvester.value = fieldId to validation
+ }
+
+ is Event.SaveToDatabase -> {
+ viewModelScope.async {
+ val playerStep = uiStates.value[tabIndex.value].toPlayerStep(gameState)
+ if (playerStep.id != -1) {
+ SupabaseRepository.savePlayerStep(playerStep)
+ }
+ }
+ }
+ }
+ }
+
+ private fun getPlayerSteps(): MutableSet {
+ val gameState =
+ gameState.value ?: throw IllegalStateException("Game state is not loaded yet!")
+ return uiStates.value.map {
+ it.toPlayerStep(gameState)
+ }.toMutableSet()
+ }
+
+ private fun isFieldIdInGame(fieldId: String): Boolean {
+ val gameState =
+ gameState.value ?: throw IllegalStateException("Game state is not loaded yet!")
+ return gameState.fields.map { it.id }.contains(fieldId)
+ }
+
+ private fun isFieldOwnedByPlayer(
+ fieldId: String,
+ playerId: Int,
+ ): Boolean {
+ val gameState =
+ gameState.value ?: throw IllegalStateException("Game state is not loaded yet!")
+ val player = gameState.players.find { it.id == playerId }
+ if (player == null) {
+ return false
+ }
+ return player.ownedFields.map { it.id }.contains(fieldId)
+ }
+
+ private fun isFieldReachableByPlayer(
+ fieldId: String,
+ playerId: Int,
+ ): Boolean {
+ val gameState =
+ gameState.value ?: throw IllegalStateException("Game state is not loaded yet!")
+ val player = gameState.players.find { it.id == playerId }
+ if (player == null) {
+ return false
+ }
+ return player.ownedFields.flatMap { it.neighbours }.map { it.id }.contains(fieldId)
+ }
+
+ private fun String.toFieldId(): String = this.toUpperCase(locale = Locale.current).filter {
+ it.isUpperCase() || it.isDigit()
+ }
+
+ private fun validateLeaveField(fieldId: String): Validation? {
+ val game = game.value ?: throw IllegalStateException("Game is not loaded yet!")
+ return when {
+ fieldId == "" -> null
+
+ !isFieldIdInGame(fieldId) -> Validation.NO_SUCH_FIELD
+
+ !isFieldOwnedByPlayer(
+ fieldId,
+ game.teams[tabIndex.value].playerId,
+ ) -> Validation.UNOWNED_FIELD
+
+ else -> null
+ }
+ }
+
+ private fun validateEnterField(fieldId: String): Validation? {
+ val game = game.value ?: throw IllegalStateException("Game is not loaded yet!")
+ return when {
+ fieldId == "" -> null
+
+ !isFieldIdInGame(fieldId) -> Validation.NO_SUCH_FIELD
+
+ !isFieldReachableByPlayer(
+ fieldId,
+ game.teams[tabIndex.value].playerId,
+ ) -> Validation.UNREACHABLE_FIELD
+
+ else -> null
+ }
+ }
+
+ private fun validatePurchaseHarvester(fieldId: String): Validation? {
+ val game = game.value ?: throw IllegalStateException("Game is not loaded yet!")
+ return when {
+ fieldId == "" -> null
+
+ !isFieldIdInGame(fieldId) -> Validation.NO_SUCH_FIELD
+
+ !isFieldOwnedByPlayer(
+ fieldId,
+ game.teams[tabIndex.value].playerId,
+ ) -> Validation.UNOWNED_FIELD
+
+ else -> null
+ }
+ }
+
+ data class EnterStepsUIState(
+ val stepId: Int = -1,
+ val playerId: Int = -1,
+ val leaveFields: SnapshotStateList> =
+ mutableStateListOf(Pair("", null)),
+ val enterFields: SnapshotStateList> =
+ mutableStateListOf(Pair("", null)),
+ val purchaseWeapons: SnapshotStateMap = mutableStateMapOf(
+ Weapon.CRYSKNIFE to 0,
+ Weapon.PISTOL to 0,
+ Weapon.LASGUN to 0,
+ Weapon.LEGION to 0,
+ ),
+ val purchaseHarvester: MutableState> =
+ mutableStateOf("" to null),
+ ) {
+ fun toPlayerStep(gameState: GameState): PlayerStep = PlayerStep(
+ id = stepId,
+ gameStateId = gameState.id,
+ playerId = playerId,
+ leaveFields = leaveFields.map { it.first }
+ .toSet(),
+ enterFields = enterFields.map { it.first }
+ .toSet(),
+ purchaseWeapons = purchaseWeapons,
+ buildHarvesters = if (purchaseHarvester.value.first !=
+ ""
+ ) {
+ setOf(purchaseHarvester.value.first)
+ } else {
+ setOf()
+ },
+ )
+ }
+
+ enum class Validation(val message: String, val isError: Boolean) {
+ NO_SUCH_FIELD("Ilyen mező nincs a pályán!", false),
+ UNREACHABLE_FIELD("A játékos nem birtokol szomszédos mezőt!", true),
+ UNOWNED_FIELD("A játékos nem birtokolja a mezőt!", true),
+ }
+
+ sealed class Event {
+ data class TabSelected(val index: Int) : Event()
+ data class RunTurn(val onComplete: () -> Unit) : Event()
+ data class LeaveField(val index: Int, val value: String) : Event()
+ data class EnterField(val index: Int, val value: String) : Event()
+ data class PurchaseWeapon(val weapon: Weapon, val value: String) : Event()
+ data class PurchaseHarvester(val value: String) : Event()
+ data object SaveToDatabase : Event()
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataScreen.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataScreen.kt
new file mode 100644
index 0000000..0e76c5d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataScreen.kt
@@ -0,0 +1,80 @@
+package hu.mokegyesulet.it.dunestrat.feature.testdata
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TestDataScreen(onTestBack: () -> Unit) {
+ val viewModel = viewModel { TestDataViewModel() }
+ val deserts by viewModel.deserts.collectAsStateWithLifecycle()
+ val games by viewModel.games.collectAsStateWithLifecycle()
+ val gameStates by viewModel.gameStates.collectAsStateWithLifecycle()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Teszt adatok") },
+ navigationIcon = {
+ IconButton(onClick = onTestBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Vissza")
+ }
+ },
+ )
+ },
+ ) { paddingValues ->
+ Column(modifier = Modifier.padding(paddingValues)) {
+ Text(
+ text = "Deserts",
+ modifier = Modifier.padding(8.dp),
+ style = MaterialTheme.typography.headlineMedium,
+ )
+ LazyColumn(modifier = Modifier.weight(1f)) {
+ items(deserts) {
+ Text(text = it.id.toString())
+ }
+ }
+ Text(
+ text = "Games",
+ modifier = Modifier.padding(8.dp),
+ style = MaterialTheme.typography.headlineMedium,
+ )
+ LazyColumn(modifier = Modifier.weight(1f)) {
+ items(games) {
+ Text(text = it.id.toString())
+ }
+ }
+ Text(
+ text = "Game states",
+ modifier = Modifier.padding(8.dp),
+ style = MaterialTheme.typography.headlineMedium,
+ )
+ LazyColumn(modifier = Modifier.weight(1f)) {
+ items(gameStates) {
+ Text(text = it.id.toString())
+ }
+ }
+
+ Button(onClick = { viewModel.initDesert() }) { Text("Init test desert") }
+ Button(onClick = { viewModel.initGame(deserts.last()) }) { Text("Init test game") }
+ Button(onClick = {
+ viewModel.initGameState(deserts.last(), games.last())
+ }) { Text("Init test gamestate") }
+ Button(onClick = {
+ viewModel.initPlayerSteps(gameStates.last())
+ }) { Text("Init test steps") }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataViewModel.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataViewModel.kt
new file mode 100644
index 0000000..2f0c5c6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/feature/testdata/TestDataViewModel.kt
@@ -0,0 +1,130 @@
+package hu.mokegyesulet.it.dunestrat.feature.testdata
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import hu.mokegyesulet.it.dunestrat.backend.SupabaseRepository
+import hu.mokegyesulet.it.dunestrat.model.*
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class TestDataViewModel : ViewModel() {
+
+ val deserts = SupabaseRepository.getDeserts()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ val games = SupabaseRepository.getGames()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ val gameStates = SupabaseRepository.getGameStates()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ private var players = (1..12).map { initPlayer(it) }
+
+ init {
+ players = (0..11).map { initPlayer(it) }
+ viewModelScope.launch {
+ games.collect { list ->
+ println("Games from Supabase: $list")
+ }
+ }
+ viewModelScope.launch {
+ deserts.collect { list ->
+ println("Deserts from Supabase: $list")
+ }
+ }
+ viewModelScope.launch {
+ gameStates.collect { list ->
+ println("Game states from Supabase: $list")
+ }
+ }
+ }
+
+ fun initDesert() {
+ val test = Desert.create6PlayerTestHexagon()
+ viewModelScope.launch {
+ SupabaseRepository.saveDesert(test)
+ }
+ }
+
+ fun initGame(desert: Desert) {
+ val playerCount = desert.fields.filter { it.startingField }.size
+ players = players.take(playerCount)
+ val names = listOf(
+ "Példa Aladár",
+ "Példa Béla",
+ "Példa Csanád",
+ "Példa Dániel",
+ "Példa Ezékiel",
+ "Példa Ferenc",
+ "Példa Gergely",
+ "Példa Hilbert",
+ "Példa Iván",
+ "Példa Jakab",
+ "Példa Károly",
+ "Példa Lajos",
+ )
+ val teams = (0..
+ Team(players[i].id, listOf(Student(names[i])))
+ }
+ viewModelScope.launch {
+ val game = Game(
+ name = "Teszt játék",
+ progress = GameProgress.INITIALIZED,
+ teams = teams,
+ desertId = desert.id,
+ )
+ SupabaseRepository.saveGame(game)
+ }
+ }
+
+ fun initGameState(
+ desert: Desert,
+ game: Game,
+ ) {
+ val stateFields = desert.fields.toGameStateFields()
+ randomlyAssignPlayers(desert, stateFields)
+ val firstState = GameState(
+ gameId = game.id,
+ index = 0,
+ fields = stateFields,
+ players = players.toSet(),
+ )
+ viewModelScope.launch {
+ SupabaseRepository.saveGameState(firstState)
+ }
+ }
+
+ private fun initPlayer(id: Int): Player = Player(
+ id,
+ 0,
+ 0,
+ 0,
+ mutableMapOf(
+ Weapon.CRYSKNIFE to 0,
+ Weapon.PISTOL to 0,
+ Weapon.LASGUN to 0,
+ Weapon.LEGION to 0,
+ ),
+ mutableSetOf(),
+ )
+ private fun randomlyAssignPlayers(
+ desert: Desert,
+ fields: Set,
+ ) {
+ val startingFields = desert.fields.filter { it.startingField }.map { it.id }.shuffled()
+ if (startingFields.size != players.size) {
+ throw RuntimeException(
+ "Start field count and player count mismatched." +
+ " Players: ${players.size}, starting fields: ${startingFields.size} \n" +
+ "Starting fields: ${startingFields.joinToString(", ")}",
+ )
+ }
+ players.zip(startingFields).forEach { (player, coordinate) ->
+ player.ownedFields.add(fields.first { it.id == coordinate })
+ }
+ }
+
+ fun initPlayerSteps(gameState: GameState): Unit =
+ throw NotImplementedError("Not implemented yet")
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Desert.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Desert.kt
new file mode 100644
index 0000000..f07a39c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Desert.kt
@@ -0,0 +1,700 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+data class Desert(
+ val id: Int = -1,
+ val name: String = "",
+ val fields: Set,
+) {
+
+ companion object {
+ fun create12PlayerHexagon(): Desert {
+ val fields = mutableSetOf()
+
+ val desertValuesByDistance = mapOf(
+ 0 to Pair(-8, 40),
+ 1 to Pair(-7, 37),
+ 2 to Pair(-6, 29),
+ 3 to Pair(-5, 19),
+ 4 to Pair(-4, 11),
+ 5 to Pair(-3, 5),
+ 6 to Pair(-2, 2),
+ 7 to Pair(-1, 1),
+ )
+
+ // exelből kimásolt
+ // ürres mezőnél Legion a hatásos
+ val effectiveWeaponsByIdOldCoordinates = mapOf(
+ "A3" to Weapon.PISTOL,
+ "A7" to Weapon.CRYSKNIFE,
+ "B2" to Weapon.CRYSKNIFE,
+ "B3" to Weapon.PISTOL,
+ "B4" to Weapon.PISTOL,
+ "B5" to Weapon.PISTOL,
+ "B6" to Weapon.CRYSKNIFE,
+ "B7" to Weapon.CRYSKNIFE,
+ "B8" to Weapon.CRYSKNIFE,
+ "B9" to Weapon.PISTOL,
+ "C1" to Weapon.LASGUN,
+ "C2" to Weapon.LASGUN,
+ "C3" to Weapon.CRYSKNIFE,
+ "C4" to Weapon.PISTOL,
+ "C5" to Weapon.PISTOL,
+ "C6" to Weapon.LASGUN,
+ "C7" to Weapon.CRYSKNIFE,
+ "C8" to Weapon.CRYSKNIFE,
+ "C9" to Weapon.PISTOL,
+ "C10" to Weapon.LASGUN,
+ "C11" to Weapon.LASGUN,
+ "D2" to Weapon.LASGUN,
+ "D3" to Weapon.LASGUN,
+ "D4" to Weapon.CRYSKNIFE,
+ "D5" to Weapon.PISTOL,
+ "D6" to Weapon.PISTOL,
+ "D7" to Weapon.CRYSKNIFE,
+ "D8" to Weapon.CRYSKNIFE,
+ "D9" to Weapon.PISTOL,
+ "D10" to Weapon.LASGUN,
+ "D11" to Weapon.LASGUN,
+ "E2" to Weapon.LASGUN,
+ "E3" to Weapon.LASGUN,
+ "E4" to Weapon.LASGUN,
+ "E5" to Weapon.CRYSKNIFE,
+ "E6" to Weapon.PISTOL,
+ "E7" to Weapon.LASGUN,
+ "E8" to Weapon.CRYSKNIFE,
+ "E9" to Weapon.PISTOL,
+ "E10" to Weapon.LASGUN,
+ "E11" to Weapon.LASGUN,
+ "E12" to Weapon.LASGUN,
+ "F2" to Weapon.CRYSKNIFE,
+ "F3" to Weapon.PISTOL,
+ "F4" to Weapon.LASGUN,
+ "F5" to Weapon.LASGUN,
+ "F6" to Weapon.CRYSKNIFE,
+ "F7" to Weapon.PISTOL,
+ "F8" to Weapon.CRYSKNIFE,
+ "F9" to Weapon.PISTOL,
+ "F10" to Weapon.LASGUN,
+ "F11" to Weapon.LASGUN,
+ "F12" to Weapon.CRYSKNIFE,
+ "F13" to Weapon.PISTOL,
+ "G1" to Weapon.CRYSKNIFE,
+ "G2" to Weapon.CRYSKNIFE,
+ "G3" to Weapon.CRYSKNIFE,
+ "G4" to Weapon.CRYSKNIFE,
+ "G5" to Weapon.PISTOL,
+ "G6" to Weapon.LASGUN,
+ "G7" to Weapon.LASGUN,
+ "G8" to Weapon.PISTOL,
+ "G9" to Weapon.CRYSKNIFE,
+ "G10" to Weapon.LASGUN,
+ "G11" to Weapon.CRYSKNIFE,
+ "G12" to Weapon.PISTOL,
+ "G13" to Weapon.PISTOL,
+ "G14" to Weapon.PISTOL,
+ "G15" to Weapon.PISTOL,
+ "H2" to Weapon.CRYSKNIFE,
+ "H3" to Weapon.CRYSKNIFE,
+ "H4" to Weapon.CRYSKNIFE,
+ "H5" to Weapon.CRYSKNIFE,
+ "H6" to Weapon.CRYSKNIFE,
+ "H7" to Weapon.CRYSKNIFE,
+ "H8" to Weapon.CRYSKNIFE,
+ "H9" to Weapon.PISTOL,
+ "H10" to Weapon.LASGUN,
+ "H11" to Weapon.PISTOL,
+ "H12" to Weapon.PISTOL,
+ "H13" to Weapon.PISTOL,
+ "H14" to Weapon.PISTOL,
+ "H15" to Weapon.PISTOL,
+ "I2" to Weapon.LASGUN,
+ "I3" to Weapon.LASGUN,
+ "I4" to Weapon.LASGUN,
+ "I5" to Weapon.LASGUN,
+ "I6" to Weapon.LASGUN,
+ "I7" to Weapon.PISTOL,
+ "I8" to Weapon.PISTOL,
+ "I9" to Weapon.LASGUN,
+ "I10" to Weapon.CRYSKNIFE,
+ "I11" to Weapon.PISTOL,
+ "I12" to Weapon.LASGUN,
+ "I13" to Weapon.LASGUN,
+ "I14" to Weapon.LASGUN,
+ "I15" to Weapon.LASGUN,
+ "I16" to Weapon.LASGUN,
+ "J2" to Weapon.PISTOL,
+ "J3" to Weapon.PISTOL,
+ "J4" to Weapon.PISTOL,
+ "J5" to Weapon.PISTOL,
+ "J6" to Weapon.PISTOL,
+ "J7" to Weapon.CRYSKNIFE,
+ "J8" to Weapon.CRYSKNIFE,
+ "J9" to Weapon.PISTOL,
+ "J10" to Weapon.LASGUN,
+ "J11" to Weapon.CRYSKNIFE,
+ "J12" to Weapon.CRYSKNIFE,
+ "J13" to Weapon.CRYSKNIFE,
+ "J14" to Weapon.CRYSKNIFE,
+ "J15" to Weapon.CRYSKNIFE,
+ "K1" to Weapon.PISTOL,
+ "K2" to Weapon.PISTOL,
+ "K3" to Weapon.PISTOL,
+ "K4" to Weapon.PISTOL,
+ "K5" to Weapon.CRYSKNIFE,
+ "K6" to Weapon.LASGUN,
+ "K7" to Weapon.LASGUN,
+ "K8" to Weapon.CRYSKNIFE,
+ "K9" to Weapon.PISTOL,
+ "K10" to Weapon.LASGUN,
+ "K11" to Weapon.PISTOL,
+ "K12" to Weapon.CRYSKNIFE,
+ "K13" to Weapon.CRYSKNIFE,
+ "K14" to Weapon.CRYSKNIFE,
+ "K15" to Weapon.CRYSKNIFE,
+ "L2" to Weapon.PISTOL,
+ "L3" to Weapon.CRYSKNIFE,
+ "L4" to Weapon.LASGUN,
+ "L5" to Weapon.LASGUN,
+ "L6" to Weapon.PISTOL,
+ "L7" to Weapon.CRYSKNIFE,
+ "L8" to Weapon.PISTOL,
+ "L9" to Weapon.CRYSKNIFE,
+ "L10" to Weapon.LASGUN,
+ "L11" to Weapon.LASGUN,
+ "L12" to Weapon.PISTOL,
+ "L13" to Weapon.CRYSKNIFE,
+ "M2" to Weapon.LASGUN,
+ "M3" to Weapon.LASGUN,
+ "M4" to Weapon.LASGUN,
+ "M5" to Weapon.PISTOL,
+ "M6" to Weapon.CRYSKNIFE,
+ "M7" to Weapon.LASGUN,
+ "M8" to Weapon.PISTOL,
+ "M9" to Weapon.CRYSKNIFE,
+ "M10" to Weapon.LASGUN,
+ "M11" to Weapon.LASGUN,
+ "M12" to Weapon.LASGUN,
+ "N2" to Weapon.LASGUN,
+ "N3" to Weapon.LASGUN,
+ "N4" to Weapon.PISTOL,
+ "N5" to Weapon.CRYSKNIFE,
+ "N6" to Weapon.CRYSKNIFE,
+ "N7" to Weapon.PISTOL,
+ "N8" to Weapon.PISTOL,
+ "N9" to Weapon.CRYSKNIFE,
+ "N10" to Weapon.LASGUN,
+ "N11" to Weapon.LASGUN,
+ "O1" to Weapon.LASGUN,
+ "O2" to Weapon.LASGUN,
+ "O3" to Weapon.PISTOL,
+ "O4" to Weapon.CRYSKNIFE,
+ "O5" to Weapon.CRYSKNIFE,
+ "O6" to Weapon.LASGUN,
+ "O7" to Weapon.PISTOL,
+ "O8" to Weapon.PISTOL,
+ "O9" to Weapon.CRYSKNIFE,
+ "O10" to Weapon.LASGUN,
+ "O11" to Weapon.LASGUN,
+ "P2" to Weapon.PISTOL,
+ "P3" to Weapon.CRYSKNIFE,
+ "P4" to Weapon.CRYSKNIFE,
+ "P5" to Weapon.CRYSKNIFE,
+ "P6" to Weapon.PISTOL,
+ "P7" to Weapon.PISTOL,
+ "P8" to Weapon.PISTOL,
+ "P9" to Weapon.CRYSKNIFE,
+ "Q3" to Weapon.CRYSKNIFE,
+ "Q7" to Weapon.PISTOL,
+ )
+
+ val effectiveWeaponsById = effectiveWeaponsByIdOldCoordinates.mapKeys {
+ convertCoordinateToNewSystem(it.key)
+ }
+
+ // exelből
+ val basesOldCoordinates = setOf(
+ "A3",
+ "A7",
+ "C1",
+ "C11",
+ "G1",
+ "G15",
+ "K1",
+ "K15",
+ "O1",
+ "O11",
+ "Q3",
+ "Q7",
+ )
+
+ // exelből
+ val mountainsOldCoordinates = mapOf(
+ "A3" to 3,
+ "A7" to 3,
+ "B2" to 7,
+ "B9" to 7,
+ "C1" to 3,
+ "C5" to 6,
+ "C6" to 6,
+ "C7" to 6,
+ "C11" to 3,
+ "D6" to 5,
+ "D7" to 5,
+ "E3" to 6,
+ "E11" to 6,
+ "F3" to 6,
+ "F4" to 5,
+ "F6" to 3,
+ "F9" to 3,
+ "F11" to 5,
+ "F12" to 6,
+ "G1" to 3,
+ "G3" to 6,
+ "G4" to 5,
+ "G12" to 5,
+ "G13" to 6,
+ "G15" to 3,
+ "I2" to 7,
+ "I6" to 3,
+ "I12" to 3,
+ "I16" to 7,
+ "K1" to 3,
+ "K3" to 6,
+ "K4" to 5,
+ "K12" to 5,
+ "K13" to 6,
+ "K15" to 3,
+ "L3" to 6,
+ "L4" to 5,
+ "L6" to 3,
+ "L9" to 3,
+ "L11" to 5,
+ "L12" to 6,
+ "M3" to 6,
+ "M11" to 6,
+ "N6" to 5,
+ "N7" to 5,
+ "O1" to 3,
+ "O5" to 6,
+ "O6" to 6,
+ "O7" to 6,
+ "O11" to 3,
+ "P2" to 7,
+ "P9" to 7,
+ "Q3" to 3,
+ "Q7" to 3,
+ )
+
+ val bases = basesOldCoordinates.map {
+ convertCoordinateToNewSystem(it)
+ }.toSet()
+
+ val mountains = mountainsOldCoordinates.mapKeys {
+ convertCoordinateToNewSystem(it.key)
+ }
+
+ val oldValues = mapOf(
+ "A3" to Pair(3, 0),
+ "A7" to Pair(3, 0),
+ "B2" to Pair(7, 0),
+ "B3" to Pair(-1, 1),
+ "B4" to Pair(-1, 1),
+ "B5" to Pair(-1, 1),
+ "B6" to Pair(-1, 1),
+ "B7" to Pair(-1, 1),
+ "B8" to Pair(-1, 1),
+ "B9" to Pair(7, 0),
+ "C1" to Pair(3, 0),
+ "C2" to Pair(-1, 1),
+ "C3" to Pair(-2, 2),
+ "C4" to Pair(-2, 2),
+ "C5" to Pair(6, 0),
+ "C6" to Pair(6, 0),
+ "C7" to Pair(6, 0),
+ "C8" to Pair(-2, 2),
+ "C9" to Pair(-2, 2),
+ "C10" to Pair(-1, 1),
+ "C11" to Pair(3, 0),
+ "D2" to Pair(-1, 1),
+ "D3" to Pair(-2, 2),
+ "D4" to Pair(-3, 5),
+ "D5" to Pair(-3, 5),
+ "D6" to Pair(5, 0),
+ "D7" to Pair(5, 0),
+ "D8" to Pair(-3, 5),
+ "D9" to Pair(-3, 5),
+ "D10" to Pair(-2, 2),
+ "D11" to Pair(-1, 1),
+ "E2" to Pair(-1, 1),
+ "E3" to Pair(6, 0),
+ "E4" to Pair(-3, 5),
+ "E5" to Pair(-4, 11),
+ "E6" to Pair(-4, 11),
+ "E7" to Pair(-4, 11),
+ "E8" to Pair(-4, 11),
+ "E9" to Pair(-4, 11),
+ "E10" to Pair(-3, 5),
+ "E11" to Pair(6, 0),
+ "E12" to Pair(-1, 1),
+ "F2" to Pair(-1, 1),
+ "F3" to Pair(6, 0),
+ "F4" to Pair(5, 0),
+ "F5" to Pair(-4, 11),
+ "F6" to Pair(3, 0),
+ "F7" to Pair(-5, 19),
+ "F8" to Pair(-5, 19),
+ "F9" to Pair(3, 0),
+ "F10" to Pair(-4, 11),
+ "F11" to Pair(5, 0),
+ "F12" to Pair(6, 0),
+ "F13" to Pair(-1, 1),
+ "G1" to Pair(3, 0),
+ "G2" to Pair(-1, 1),
+ "G3" to Pair(6, 0),
+ "G4" to Pair(5, 0),
+ "G5" to Pair(-4, 11),
+ "G6" to Pair(-5, 19),
+ "G7" to Pair(-6, 29),
+ "G8" to Pair(-6, 29),
+ "G9" to Pair(-6, 29),
+ "G10" to Pair(-5, 19),
+ "G11" to Pair(-4, 11),
+ "G12" to Pair(5, 0),
+ "G13" to Pair(6, 0),
+ "G14" to Pair(-1, 1),
+ "G15" to Pair(3, 0),
+ "H2" to Pair(-1, 1),
+ "H3" to Pair(-2, 2),
+ "H4" to Pair(-3, 5),
+ "H5" to Pair(-4, 11),
+ "H6" to Pair(-5, 19),
+ "H7" to Pair(-6, 29),
+ "H8" to Pair(-7, 37),
+ "H9" to Pair(-7, 37),
+ "H10" to Pair(-6, 29),
+ "H11" to Pair(-5, 19),
+ "H12" to Pair(-4, 11),
+ "H13" to Pair(-3, 5),
+ "H14" to Pair(-2, 2),
+ "H15" to Pair(-1, 1),
+ "I2" to Pair(7, 0),
+ "I3" to Pair(-2, 2),
+ "I4" to Pair(-3, 5),
+ "I5" to Pair(-4, 11),
+ "I6" to Pair(3, 0),
+ "I7" to Pair(-6, 29),
+ "I8" to Pair(-7, 37),
+ "I9" to Pair(-8, 40),
+ "I10" to Pair(-7, 37),
+ "I11" to Pair(-6, 29),
+ "I12" to Pair(3, 0),
+ "I13" to Pair(-4, 11),
+ "I14" to Pair(-3, 5),
+ "I15" to Pair(-2, 2),
+ "I16" to Pair(7, 0),
+ "J2" to Pair(-1, 1),
+ "J3" to Pair(-2, 2),
+ "J4" to Pair(-3, 5),
+ "J5" to Pair(-4, 11),
+ "J6" to Pair(-5, 19),
+ "J7" to Pair(-6, 29),
+ "J8" to Pair(-7, 37),
+ "J9" to Pair(-7, 37),
+ "J10" to Pair(-6, 29),
+ "J11" to Pair(-5, 19),
+ "J12" to Pair(-4, 11),
+ "J13" to Pair(-3, 5),
+ "J14" to Pair(-2, 2),
+ "J15" to Pair(-1, 1),
+ "K1" to Pair(3, 0),
+ "K2" to Pair(-1, 1),
+ "K3" to Pair(6, 0),
+ "K4" to Pair(5, 0),
+ "K5" to Pair(-4, 11),
+ "K6" to Pair(-5, 19),
+ "K7" to Pair(-6, 29),
+ "K8" to Pair(-6, 29),
+ "K9" to Pair(-6, 29),
+ "K10" to Pair(-5, 19),
+ "K11" to Pair(-4, 11),
+ "K12" to Pair(5, 0),
+ "K13" to Pair(6, 0),
+ "K14" to Pair(-1, 1),
+ "K15" to Pair(3, 0),
+ "L2" to Pair(-1, 1),
+ "L3" to Pair(6, 0),
+ "L4" to Pair(5, 0),
+ "L5" to Pair(-4, 11),
+ "L6" to Pair(3, 0),
+ "L7" to Pair(-5, 19),
+ "L8" to Pair(-5, 19),
+ "L9" to Pair(3, 0),
+ "L10" to Pair(-4, 11),
+ "L11" to Pair(5, 0),
+ "L12" to Pair(6, 0),
+ "L13" to Pair(-1, 1),
+ "M2" to Pair(-1, 1),
+ "M3" to Pair(6, 0),
+ "M4" to Pair(-3, 5),
+ "M5" to Pair(-4, 11),
+ "M6" to Pair(-4, 11),
+ "M7" to Pair(-4, 11),
+ "M8" to Pair(-4, 11),
+ "M9" to Pair(-4, 11),
+ "M10" to Pair(-3, 5),
+ "M11" to Pair(6, 0),
+ "M12" to Pair(-1, 1),
+ "N2" to Pair(-1, 1),
+ "N3" to Pair(-2, 2),
+ "N4" to Pair(-3, 5),
+ "N5" to Pair(-3, 5),
+ "N6" to Pair(5, 0),
+ "N7" to Pair(5, 0),
+ "N8" to Pair(-3, 5),
+ "N9" to Pair(-3, 5),
+ "N10" to Pair(-2, 2),
+ "N11" to Pair(-1, 1),
+ "O1" to Pair(3, 0),
+ "O2" to Pair(-1, 1),
+ "O3" to Pair(-2, 2),
+ "O4" to Pair(-2, 2),
+ "O5" to Pair(6, 0),
+ "O6" to Pair(6, 0),
+ "O7" to Pair(6, 0),
+ "O8" to Pair(-2, 2),
+ "O9" to Pair(-2, 2),
+ "O10" to Pair(-1, 1),
+ "O11" to Pair(3, 0),
+ "P2" to Pair(7, 0),
+ "P3" to Pair(-1, 1),
+ "P4" to Pair(-1, 1),
+ "P5" to Pair(-1, 1),
+ "P6" to Pair(-1, 1),
+ "P7" to Pair(-1, 1),
+ "P8" to Pair(-1, 1),
+ "P9" to Pair(7, 0),
+ "Q3" to Pair(3, 0),
+ "Q7" to Pair(3, 0),
+ )
+
+ val values = oldValues.mapKeys {
+ convertCoordinateToNewSystem(it.key)
+ }
+
+ effectiveWeaponsById.forEach { (id, weapon) ->
+ if (id in bases) {
+ fields.add(
+ DesertField(
+ id = id,
+ water = 4,
+ spice = 0,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = true,
+ ),
+ )
+ } else if (id in mountains.keys) {
+ fields.add(
+ DesertField(
+ id = id,
+ water = mountains[id]!!,
+ spice = 0,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = false,
+ ),
+ )
+ } else {
+ val (water, spice) = values[id] ?: Pair(-1, 1)
+
+ fields.add(
+ DesertField(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = false,
+ ),
+ )
+ }
+ }
+
+ effectiveWeaponsById.forEach { (id, weapon) ->
+ if (id in bases) {
+ fields.add(
+ DesertField(
+ id = id,
+ water = 4,
+ spice = 0,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = true,
+ ),
+ )
+ } else if (id in mountains.keys) {
+ fields.add(
+ DesertField(
+ id = id,
+ water = mountains[id]!!,
+ spice = 0,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = false,
+ ),
+ )
+ } else {
+ val (water, spice) = values[id] ?: Pair(-1, 1)
+
+ fields.add(
+ DesertField(
+ id = id,
+ water = water,
+ spice = spice,
+ effectiveWeapon = weapon,
+ neighbours = mutableSetOf(),
+ startingField = false,
+ ),
+ )
+ }
+ }
+
+ fields.forEach { field ->
+
+ val fieldFirstCord = field.id[0].code
+ val fieldSecondCord = field.id.substring(1).toInt()
+
+ val neighbourFirstCords = fieldFirstCord - 1..fieldFirstCord + 1
+
+ val neighbourSecondCords = fieldSecondCord - 1..fieldSecondCord + 1
+
+ val neighbours = fields.filter { entry ->
+ (entry.id[0].code in neighbourFirstCords) and
+ (entry.id.substring(1).toInt() in neighbourSecondCords) and
+ !(
+ (
+ (entry.id[0].code == fieldFirstCord - 1) and
+ (entry.id.substring(1).toInt() == fieldSecondCord + 1)
+ ) or (
+ (entry.id[0].code == fieldFirstCord + 1) and
+ (entry.id.substring(1).toInt() == fieldSecondCord - 1)
+ ) or (
+ (entry.id[0].code == fieldFirstCord) and
+ (entry.id.substring(1).toInt() == fieldSecondCord)
+ )
+ )
+ }
+
+ field.neighbours.addAll(neighbours)
+ }
+
+ return Desert(
+ name = "Standard 12 játékos",
+ fields = fields,
+ )
+ }
+
+ fun convertCoordinateToNewSystem(
+ oldCoordinate: String,
+ middleColumnLetter: Char = 'I',
+ ): String {
+ val letter = oldCoordinate[0]
+ val number = oldCoordinate.substring(1).toInt()
+ if (letter <= middleColumnLetter) {
+ return oldCoordinate
+ }
+ val shift = letter - middleColumnLetter
+ val newNumber = number + shift
+ return "$letter$newNumber"
+ }
+
+ fun create6PlayerTestHexagon(): Desert {
+ val outer: (
+ String,
+ ) -> DesertField = { id ->
+ DesertField(
+ id,
+ 5,
+ 0,
+ Weapon.LASGUN,
+ mutableSetOf(),
+ true,
+ )
+ }
+ val inner: (
+ String,
+ ) -> DesertField = { id ->
+ DesertField(
+ id,
+ -2,
+ 5,
+ Weapon.PISTOL,
+ mutableSetOf(),
+ false,
+ )
+ }
+ val fields = mutableSetOf()
+ val center = DesertField(
+ "C3",
+ -5,
+ 15,
+ Weapon.CRYSKNIFE,
+ mutableSetOf(),
+ false,
+ )
+ fields.add(center)
+ listOf("B2", "C2", "D3", "D4", "C4", "B3").forEach { id ->
+ val f = inner(id)
+ val prev = fields.last()
+ prev.neighbours.add(f)
+ f.neighbours.add(prev)
+ fields.add(f)
+ f.neighbours.add(center)
+ center.neighbours.add(f)
+ if (id == "B3") {
+ val b2 = fields.first { it.id == "B2" }
+ f.neighbours.add(b2)
+ b2.neighbours.add(f)
+ }
+ }
+ val b2 = fields.first { it.id == "B2" }
+ val c2 = fields.first { it.id == "C2" }
+ val d3 = fields.first { it.id == "D3" }
+ val d4 = fields.first { it.id == "D4" }
+ val c4 = fields.first { it.id == "C4" }
+ val b3 = fields.first { it.id == "B3" }
+ val b1 = outer("B1")
+ val d2 = outer("D2")
+ val e4 = outer("E4")
+ val d5 = outer("D5")
+ val b4 = outer("B4")
+ val a2 = outer("A2")
+ fields.addAll(listOf(b1, d2, e4, d5, b4, a2))
+ b1.neighbours.add(b2)
+ b2.neighbours.add(b1)
+ b1.neighbours.add(c2)
+ c2.neighbours.add(b1)
+ d2.neighbours.add(c2)
+ c2.neighbours.add(d2)
+ d2.neighbours.add(d3)
+ d3.neighbours.add(d2)
+ e4.neighbours.add(d3)
+ d3.neighbours.add(e4)
+ e4.neighbours.add(d4)
+ d4.neighbours.add(e4)
+ d5.neighbours.add(d4)
+ d4.neighbours.add(d5)
+ d5.neighbours.add(c4)
+ c4.neighbours.add(d5)
+ b4.neighbours.add(c4)
+ c4.neighbours.add(b4)
+ b4.neighbours.add(b3)
+ b3.neighbours.add(b4)
+ a2.neighbours.add(b3)
+ b3.neighbours.add(a2)
+ a2.neighbours.add(b2)
+ b2.neighbours.add(a2)
+ return Desert(fields = fields, name = "Teszt (6 játékos)")
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/DesertField.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/DesertField.kt
new file mode 100644
index 0000000..a263dc1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/DesertField.kt
@@ -0,0 +1,13 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class DesertField(
+ var id: String,
+ var water: Int,
+ var spice: Int,
+ var effectiveWeapon: Weapon,
+ val neighbours: MutableSet,
+ var startingField: Boolean,
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Game.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Game.kt
new file mode 100644
index 0000000..855a562
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Game.kt
@@ -0,0 +1,23 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+@Serializable
+data class Game(
+ @Transient
+ val id: Int = -1,
+ val name: String,
+ @Transient
+ val progress: GameProgress = GameProgress.ERROR,
+ val teams: List,
+ @Transient
+ val desertId: Int = -1,
+)
+
+enum class GameProgress {
+ INITIALIZED,
+ ONGOING,
+ FINISHED,
+ ERROR,
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameState.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameState.kt
new file mode 100644
index 0000000..8b1ddd6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameState.kt
@@ -0,0 +1,190 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+data class GameState(
+ val id: Int = -1,
+ val gameId: Int,
+ var index: Int,
+ val fields: Set,
+ val players: Set,
+) {
+
+ fun runTurn(playerSteps: MutableSet): GameState {
+ this.leaveFields(playerSteps)
+ this.waterConsumption(playerSteps)
+ this.checkPurchases(playerSteps)
+ this.buildHarvesters(playerSteps)
+ this.purchaseWeapons(playerSteps)
+ this.produceSpice()
+ this.expansions(playerSteps)
+ this.deliverWeapons(playerSteps)
+ index++
+
+ return GameState(
+ gameId = gameId,
+ index = index,
+ fields = fields,
+ players = players,
+ )
+ }
+
+ fun leaveFields(playerSteps: Set) {
+ players.forEach { player ->
+ player.leaveFields(getPlayerStepById(player.id, playerSteps).leaveFields)
+ }
+ }
+
+ fun waterConsumption(playerSteps: MutableSet) {
+ players.forEach { player ->
+ if (player.waterConsumption()) {
+ playerSteps.removeAll { it.playerId == player.id }
+ playerSteps.add(
+ PlayerStep(
+ gameStateId = -1,
+ playerId = player.id,
+ leaveFields = setOf(),
+ enterFields = setOf(),
+ purchaseWeapons = mapOf(),
+ buildHarvesters = setOf(),
+ ),
+ )
+ }
+ }
+ }
+
+ fun getPlayerStepById(
+ playerId: Int,
+ playerSteps: Set,
+ ): PlayerStep = playerSteps.find { playerStep ->
+ playerStep.playerId == playerId
+ } ?: PlayerStep(
+ gameStateId = -1,
+ playerId = playerId,
+ leaveFields = setOf(),
+ enterFields = setOf(),
+ purchaseWeapons = mapOf(),
+ buildHarvesters = setOf(),
+ )
+
+ fun checkPurchases(playerSteps: Set) {
+ players.forEach { player ->
+ player.inDebt =
+ player.spice < player.calculatePrices(getPlayerStepById(player.id, playerSteps))
+ }
+ }
+
+ fun buildHarvesters(playerSteps: Set) {
+ players.filter { !it.inDebt }.forEach { player ->
+ getPlayerStepById(player.id, playerSteps).buildHarvesters.forEach { fieldId ->
+ val field =
+ player.ownedFields.find { field -> field.id == fieldId } ?: return@forEach
+
+ player.purchaseHarvester(field)
+ }
+ }
+ }
+
+ fun purchaseWeapons(playerSteps: Set) {
+ players.filter { !it.inDebt }.forEach { player ->
+ player.purchaseWeapons(getPlayerStepById(player.id, playerSteps).purchaseWeapons)
+ }
+ }
+
+ fun produceSpice() {
+ players.forEach { player ->
+ player.ownedFields.forEach { field ->
+ player.spice += field.spice
+ }
+ }
+ }
+
+ fun deliverWeapons(playerSteps: Set) {
+ players.filter { !it.inDebt }.forEach { player ->
+ player.deliverWeapons(getPlayerStepById(player.id, playerSteps).purchaseWeapons)
+ }
+ }
+
+ fun expansions(playerSteps: Set) {
+ val chosenFields = mutableMapOf>()
+ val losses = mutableMapOf()
+
+ players.forEach { player ->
+ losses[player] = 0.0
+ }
+
+ fields.forEach { field ->
+ chosenFields[field] = mutableSetOf()
+ }
+
+ players.filter { !it.inDebt }
+ .forEach { player ->
+ getPlayerStepById(player.id, playerSteps).enterFields
+ .mapNotNull { fieldId ->
+ fields.find { field -> field.id == fieldId }
+ }
+ .filter { field ->
+ player.isFieldReachable(field)
+ }
+ .forEach { field -> chosenFields[field]?.add(player) }
+ }
+
+ chosenFields.filter { it.value.isNotEmpty() }
+ .forEach { entry ->
+ val field = entry.key
+ val players = entry.value
+
+ if (players.isNotEmpty()) {
+ val occupant = getOccupant(field)
+ val max = players.maxOfOrNull { it.calculatePower(field) } ?: 0
+ val winningPlayers: List = players.filter { player ->
+ player.calculatePower(field) == max
+ }
+ val newOccupant = modifyFieldOwnershipsAfterBattle(field, winningPlayers)
+ players.forEach { player ->
+ losses[player] =
+ (losses[player] ?: 0.0) +
+ calculateLossOnField(player, occupant, newOccupant)
+ }
+ }
+ }
+ }
+
+ fun getOccupant(field: GameStateField): Player? {
+ players.forEach { player ->
+ if (field in player.ownedFields) {
+ return player
+ }
+ }
+ return null
+ }
+
+ private fun modifyFieldOwnershipsAfterBattle(
+ field: GameStateField,
+ winningPlayers: List,
+ ): Player? {
+ val occupant = getOccupant(field)
+ if (occupant in winningPlayers) {
+ return occupant
+ }
+ occupant?.ownedFields?.remove(field)
+ if (winningPlayers.size >= 2) {
+ return null
+ }
+ val newOccupant = winningPlayers[0]
+ newOccupant.ownedFields.add(field)
+ return newOccupant
+ }
+
+ private fun calculateLossOnField(
+ player: Player,
+ previousOccupant: Player?,
+ newOccupant: Player?,
+ ): Double {
+ if (previousOccupant == player) {
+ return 0.0
+ }
+ if (newOccupant == player) {
+ return 0.2
+ }
+ return 0.1
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameStateField.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameStateField.kt
new file mode 100644
index 0000000..4446cf6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/GameStateField.kt
@@ -0,0 +1,97 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+@Serializable(with = GameStateFieldSerializer::class)
+class GameStateField(
+ val id: String,
+ val water: Int,
+ val spice: Int,
+ val effectiveWeapon: Weapon,
+ var harvester: Boolean,
+ val neighbours: MutableSet,
+) {
+ fun copy(
+ id: String = this.id,
+ water: Int = this.water,
+ spice: Int = this.spice,
+ effectiveWeapon: Weapon = this.effectiveWeapon,
+ harvester: Boolean = this.harvester,
+ neighbours: Set = this.neighbours,
+ ): GameStateField = GameStateField(
+ id,
+ water,
+ spice,
+ effectiveWeapon,
+ harvester,
+ neighbours.toMutableSet(),
+ )
+}
+
+@Serializable
+private data class GameStateFieldSurrogate(
+ val id: String,
+ val water: Int,
+ val spice: Int,
+ val effectiveWeapon: Weapon,
+ var harvester: Boolean,
+ val neighbours: List,
+)
+
+object GameStateFieldSerializer : KSerializer {
+ override val descriptor: SerialDescriptor = GameStateFieldSurrogate.serializer().descriptor
+
+ override fun serialize(
+ encoder: Encoder,
+ value: GameStateField,
+ ) {
+ val surrogate = GameStateFieldSurrogate(
+ id = value.id,
+ water = value.water,
+ spice = value.spice,
+ effectiveWeapon = value.effectiveWeapon,
+ harvester = value.harvester,
+ neighbours = value.neighbours.map { it.id },
+ )
+ encoder.encodeSerializableValue(GameStateFieldSurrogate.serializer(), surrogate)
+ }
+
+ override fun deserialize(decoder: Decoder): GameStateField {
+ val surrogate = decoder.decodeSerializableValue(GameStateFieldSurrogate.serializer())
+ return GameStateField(
+ id = surrogate.id,
+ water = surrogate.water,
+ spice = surrogate.spice,
+ effectiveWeapon = surrogate.effectiveWeapon,
+ harvester = surrogate.harvester,
+ neighbours = mutableSetOf(),
+ )
+ }
+}
+
+fun Set.toGameStateFields(): Set {
+ val gameStateFields = this.map {
+ GameStateField(
+ it.id,
+ it.water,
+ it.spice,
+ it.effectiveWeapon,
+ false,
+ mutableSetOf(),
+ )
+ }.toSet()
+ for (stateField in gameStateFields) {
+ val neighbors =
+ this
+ .first { it.id == stateField.id }
+ .neighbours
+ .map { neighbor -> gameStateFields.first({ it.id == neighbor.id }) }
+ .toSet()
+ stateField.neighbours.addAll(neighbors)
+ }
+ return gameStateFields
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Player.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Player.kt
new file mode 100644
index 0000000..e5593e3
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Player.kt
@@ -0,0 +1,96 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlin.math.pow
+import kotlinx.serialization.Transient
+
+data class Player(
+ val id: Int,
+ var water: Int,
+ var spice: Int,
+ var harvestersPurchased: Int,
+ val weapons: MutableMap,
+ val ownedFields: MutableSet,
+ @Transient
+ var inDebt: Boolean = false,
+) {
+ fun getWeaponCount(weapon: Weapon): Int = weapons[weapon] ?: 0
+
+ fun leaveFields(fields: Set) {
+ ownedFields.removeAll { field -> field.id in fields }
+ }
+
+ fun waterConsumption(): Boolean {
+ var waterChange = 0
+ ownedFields.forEach { field -> waterChange += field.water }
+ water += waterChange
+
+ // return whether there is an issue or not
+ if (water >= 0) {
+ return false
+ }
+ ownedFields.removeAll { field -> field.water < 0 }
+ water = 0
+ return true
+ }
+
+ fun purchaseWeapons(purchaseWeapons: Map) {
+ spice -= calculateWeaponPrices(purchaseWeapons)
+ }
+
+ fun deliverWeapons(purchaseWeapons: Map) {
+ purchaseWeapons.keys.forEach { weapon ->
+ weapons[weapon] = getWeaponCount(weapon) + (purchaseWeapons[weapon] ?: 0)
+ }
+ }
+
+ fun purchaseHarvester(purchaseField: GameStateField) {
+ purchaseField.harvester = true
+ spice -= 5 * 2.0.pow(harvestersPurchased++).toInt()
+ }
+
+ fun calculateWeaponPrices(purchaseWeapons: Map): Int {
+ var sum = 0
+ purchaseWeapons.forEach { entry ->
+ sum += entry.value * entry.key.price
+ }
+ return sum
+ }
+
+ fun calculateHarvesterPrice(count: Int): Int =
+ (5 * 2.0.pow(harvestersPurchased) * (2.0.pow(count) - 1)).toInt()
+
+ fun calculatePrices(purchases: PlayerStep): Int =
+ calculateWeaponPrices(purchases.purchaseWeapons) +
+ calculateHarvesterPrice(purchases.buildHarvesters.size)
+
+ fun calculatePower(field: GameStateField): Int {
+ var sum = getWeaponCount(field.effectiveWeapon) * 2
+ if (field !in ownedFields) {
+ sum += getWeaponCount(Weapon.LEGION) * 3
+ }
+ weapons.filter { weapon -> weapon.key != Weapon.LEGION }.keys.forEach { weapon ->
+ sum += getWeaponCount(weapon)
+ }
+ return sum
+ }
+
+ fun isFieldReachable(fieldInQuestion: GameStateField): Boolean {
+ if (fieldInQuestion in ownedFields) {
+ return false
+ }
+ ownedFields.forEach { field ->
+ if (fieldInQuestion in field.neighbours) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun loseWeaponPrecent(percent: Double) {
+ val normalizedPercent = if (percent > 1.0) 1.0 else percent
+ weapons.keys.forEach { weapon ->
+ weapons[weapon] = ((weapons[weapon]?.toDouble() ?: 0.0) * (1 - normalizedPercent))
+ .toInt()
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/PlayerStep.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/PlayerStep.kt
new file mode 100644
index 0000000..e1f9147
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/PlayerStep.kt
@@ -0,0 +1,18 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+@Serializable
+data class PlayerStep(
+ @Transient
+ val id: Int = -1,
+ @Transient
+ val gameStateId: Int = -1,
+ @Transient
+ val playerId: Int = -1,
+ var leaveFields: Set = emptySet(),
+ var enterFields: Set = emptySet(),
+ var purchaseWeapons: Map = emptyMap(),
+ var buildHarvesters: Set = emptySet(),
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Student.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Student.kt
new file mode 100644
index 0000000..03aef2b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Student.kt
@@ -0,0 +1,14 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class Student(
+ val name: String = "Ezekiel",
+ val batkabankId: String = "",
+) {
+ fun copy(
+ name: String = this.name,
+ batkabankId: String = this.batkabankId,
+ ) = Student(name, batkabankId)
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Team.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Team.kt
new file mode 100644
index 0000000..417c928
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Team.kt
@@ -0,0 +1,9 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class Team(
+ val playerId: Int,
+ val students: List,
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Weapon.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Weapon.kt
new file mode 100644
index 0000000..79bf13b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/model/Weapon.kt
@@ -0,0 +1,8 @@
+package hu.mokegyesulet.it.dunestrat.model
+
+enum class Weapon(val price: Int) {
+ PISTOL(1),
+ LASGUN(1),
+ CRYSKNIFE(1),
+ LEGION(2),
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/NavGraph.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/NavGraph.kt
new file mode 100644
index 0000000..3f7e727
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/NavGraph.kt
@@ -0,0 +1,70 @@
+package hu.mokegyesulet.it.dunestrat.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
+import hu.mokegyesulet.it.dunestrat.feature.init.InitScreen
+import hu.mokegyesulet.it.dunestrat.feature.inventory.InventoryScreen
+import hu.mokegyesulet.it.dunestrat.feature.mainmenu.MainMenu
+import hu.mokegyesulet.it.dunestrat.feature.playerstep.PlayerStepInputScreen
+import hu.mokegyesulet.it.dunestrat.feature.testdata.TestDataScreen
+
+@Composable
+fun NavGraph(navController: NavHostController = rememberNavController()) {
+ NavHost(
+ navController = navController,
+ startDestination = Screen.MainMenu,
+ ) {
+ composable {
+ MainMenu(
+ onStat = {},
+ onInputMovesClick = { gameId ->
+ navController.navigate(Screen.PlayerStepInput(gameId))
+ },
+ onNewGameClick = {
+ navController.navigate(Screen.InitScreen)
+ },
+ onInventoryClick = { gameId ->
+ navController.navigate(Screen.Inventory(gameId))
+ },
+ )
+ }
+
+ composable {
+ InitScreen(
+ onNavigateBack = {
+ navController.navigate(Screen.MainMenu) {
+ popUpTo(Screen.MainMenu) { inclusive = true }
+ }
+ },
+ )
+ }
+
+ composable { backStackEntry ->
+
+ val navigationObject: Screen.PlayerStepInput = backStackEntry.toRoute()
+
+ PlayerStepInputScreen(
+ gameId = navigationObject.gameId,
+ onBack = {
+ navController.popBackStack()
+ },
+ onRunTurn = {
+ navController.popBackStack()
+ navController.navigate(Screen.Inventory(navigationObject.gameId))
+ },
+ )
+ }
+ composable { backStackEntry ->
+ val navigationObject: Screen.Inventory = backStackEntry.toRoute()
+ InventoryScreen(
+ gameId = navigationObject.gameId,
+ onBack = { navController.popBackStack() },
+ )
+ }
+ composable { TestDataScreen(onTestBack = { navController.navigateUp() }) }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/Screen.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/Screen.kt
new file mode 100644
index 0000000..6d47f23
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/Screen.kt
@@ -0,0 +1,21 @@
+package hu.mokegyesulet.it.dunestrat.navigation
+
+import kotlinx.serialization.Serializable
+
+sealed class Screen {
+
+ @Serializable
+ data object MainMenu : Screen()
+
+ @Serializable
+ data object TestData : Screen()
+
+ @Serializable
+ data object InitScreen : Screen()
+
+ @Serializable
+ data class PlayerStepInput(val gameId: Int) : Screen()
+
+ @Serializable
+ data class Inventory(val gameId: Int) : Screen()
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/dev.md b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/dev.md
new file mode 100644
index 0000000..ed141d2
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/navigation/dev.md
@@ -0,0 +1,17 @@
+To add a new composable to the navigation follow these steps:
+- Create a data object/class for it in `Screen.kt`
+ - If you don't need additional information from navigation use a data object: `data object YourComponentsName : Screen()`
+ - If you need some additional info (e.g. id of an object) use a data class: `data class YourComponentsName(val objectId: Int, ... ): Screen()`
+- Create a `composable` entry in `NavGraph.kt`
+ - Add your composable to it
+ - You can access your navigation arguments like that:
+ ```kotlin
+ composable { backStackEntry ->
+ val navigationObject: Screen.YourComponentsName = backStackEntry.toRoute()
+
+ YourComponentsNameScreen(navigationObject.objectid, ...)
+ }
+ ```
+- Fill in the lambdas for navigation
+ - Use `navController.navigate(Screen.OtherComponentsName)` to navigate to other components
+ - Use `navController.popBackStack()` to navigate to the previously displayed component before your component
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Color.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Color.kt
new file mode 100644
index 0000000..55b79fb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Color.kt
@@ -0,0 +1,218 @@
+package hu.mokegyesulet.it.dunestrat.ui
+import androidx.compose.ui.graphics.Color
+
+val primaryLight = Color(0xFF6D5E0F)
+val onPrimaryLight = Color(0xFFFFFFFF)
+val primaryContainerLight = Color(0xFFF8E287)
+val onPrimaryContainerLight = Color(0xFF534600)
+val secondaryLight = Color(0xFF665E40)
+val onSecondaryLight = Color(0xFFFFFFFF)
+val secondaryContainerLight = Color(0xFFEEE2BC)
+val onSecondaryContainerLight = Color(0xFF4E472A)
+val tertiaryLight = Color(0xFF43664F)
+val onTertiaryLight = Color(0xFFFFFFFF)
+val tertiaryContainerLight = Color(0xFFC5ECCE)
+val onTertiaryContainerLight = Color(0xFF2C4E38)
+val errorLight = Color(0xFFBA1A1A)
+val onErrorLight = Color(0xFFFFFFFF)
+val errorContainerLight = Color(0xFFFFDAD6)
+val onErrorContainerLight = Color(0xFF93000A)
+val backgroundLight = Color(0xFFFFF9EE)
+val onBackgroundLight = Color(0xFF1E1B13)
+val surfaceLight = Color(0xFFFFF9EE)
+val onSurfaceLight = Color(0xFF1E1B13)
+val surfaceVariantLight = Color(0xFFE9E2D0)
+val onSurfaceVariantLight = Color(0xFF4B4739)
+val outlineLight = Color(0xFF7C7767)
+val outlineVariantLight = Color(0xFFCDC6B4)
+val scrimLight = Color(0xFF000000)
+val inverseSurfaceLight = Color(0xFF333027)
+val inverseOnSurfaceLight = Color(0xFFF7F0E2)
+val inversePrimaryLight = Color(0xFFDBC66F)
+val surfaceDimLight = Color(0xFFE0D9CC)
+val surfaceBrightLight = Color(0xFFFFF9EE)
+val surfaceContainerLowestLight = Color(0xFFFFFFFF)
+val surfaceContainerLowLight = Color(0xFFFAF3E5)
+val surfaceContainerLight = Color(0xFFF4EDDF)
+val surfaceContainerHighLight = Color(0xFFEEE8DA)
+val surfaceContainerHighestLight = Color(0xFFE8E2D4)
+
+val primaryLightMediumContrast = Color(0xFF403600)
+val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
+val primaryContainerLightMediumContrast = Color(0xFF7D6C1E)
+val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val secondaryLightMediumContrast = Color(0xFF3C361B)
+val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
+val secondaryContainerLightMediumContrast = Color(0xFF756D4E)
+val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val tertiaryLightMediumContrast = Color(0xFF1B3D28)
+val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
+val tertiaryContainerLightMediumContrast = Color(0xFF52755D)
+val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val errorLightMediumContrast = Color(0xFF740006)
+val onErrorLightMediumContrast = Color(0xFFFFFFFF)
+val errorContainerLightMediumContrast = Color(0xFFCF2C27)
+val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
+val backgroundLightMediumContrast = Color(0xFFFFF9EE)
+val onBackgroundLightMediumContrast = Color(0xFF1E1B13)
+val surfaceLightMediumContrast = Color(0xFFFFF9EE)
+val onSurfaceLightMediumContrast = Color(0xFF131109)
+val surfaceVariantLightMediumContrast = Color(0xFFE9E2D0)
+val onSurfaceVariantLightMediumContrast = Color(0xFF3A3629)
+val outlineLightMediumContrast = Color(0xFF575244)
+val outlineVariantLightMediumContrast = Color(0xFF726D5E)
+val scrimLightMediumContrast = Color(0xFF000000)
+val inverseSurfaceLightMediumContrast = Color(0xFF333027)
+val inverseOnSurfaceLightMediumContrast = Color(0xFFF7F0E2)
+val inversePrimaryLightMediumContrast = Color(0xFFDBC66F)
+val surfaceDimLightMediumContrast = Color(0xFFCCC6B9)
+val surfaceBrightLightMediumContrast = Color(0xFFFFF9EE)
+val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
+val surfaceContainerLowLightMediumContrast = Color(0xFFFAF3E5)
+val surfaceContainerLightMediumContrast = Color(0xFFEEE8DA)
+val surfaceContainerHighLightMediumContrast = Color(0xFFE3DCCF)
+val surfaceContainerHighestLightMediumContrast = Color(0xFFD7D1C4)
+
+val primaryLightHighContrast = Color(0xFF352C00)
+val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
+val primaryContainerLightHighContrast = Color(0xFF564800)
+val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val secondaryLightHighContrast = Color(0xFF322C12)
+val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
+val secondaryContainerLightHighContrast = Color(0xFF50492D)
+val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val tertiaryLightHighContrast = Color(0xFF10321F)
+val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
+val tertiaryContainerLightHighContrast = Color(0xFF2E503A)
+val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val errorLightHighContrast = Color(0xFF600004)
+val onErrorLightHighContrast = Color(0xFFFFFFFF)
+val errorContainerLightHighContrast = Color(0xFF98000A)
+val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
+val backgroundLightHighContrast = Color(0xFFFFF9EE)
+val onBackgroundLightHighContrast = Color(0xFF1E1B13)
+val surfaceLightHighContrast = Color(0xFFFFF9EE)
+val onSurfaceLightHighContrast = Color(0xFF000000)
+val surfaceVariantLightHighContrast = Color(0xFFE9E2D0)
+val onSurfaceVariantLightHighContrast = Color(0xFF000000)
+val outlineLightHighContrast = Color(0xFF2F2C20)
+val outlineVariantLightHighContrast = Color(0xFF4D493B)
+val scrimLightHighContrast = Color(0xFF000000)
+val inverseSurfaceLightHighContrast = Color(0xFF333027)
+val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
+val inversePrimaryLightHighContrast = Color(0xFFDBC66F)
+val surfaceDimLightHighContrast = Color(0xFFBEB8AB)
+val surfaceBrightLightHighContrast = Color(0xFFFFF9EE)
+val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
+val surfaceContainerLowLightHighContrast = Color(0xFFF7F0E2)
+val surfaceContainerLightHighContrast = Color(0xFFE8E2D4)
+val surfaceContainerHighLightHighContrast = Color(0xFFDAD4C6)
+val surfaceContainerHighestLightHighContrast = Color(0xFFCCC6B9)
+
+val primaryDark = Color(0xFFDBC66F)
+val onPrimaryDark = Color(0xFF393000)
+val primaryContainerDark = Color(0xFF534600)
+val onPrimaryContainerDark = Color(0xFFF8E287)
+val secondaryDark = Color(0xFFD1C6A1)
+val onSecondaryDark = Color(0xFF363016)
+val secondaryContainerDark = Color(0xFF4E472A)
+val onSecondaryContainerDark = Color(0xFFEEE2BC)
+val tertiaryDark = Color(0xFFA9D0B3)
+val onTertiaryDark = Color(0xFF143723)
+val tertiaryContainerDark = Color(0xFF2C4E38)
+val onTertiaryContainerDark = Color(0xFFC5ECCE)
+val errorDark = Color(0xFFFFB4AB)
+val onErrorDark = Color(0xFF690005)
+val errorContainerDark = Color(0xFF93000A)
+val onErrorContainerDark = Color(0xFFFFDAD6)
+val backgroundDark = Color(0xFF15130B)
+val onBackgroundDark = Color(0xFFE8E2D4)
+val surfaceDark = Color(0xFF15130B)
+val onSurfaceDark = Color(0xFFE8E2D4)
+val surfaceVariantDark = Color(0xFF4B4739)
+val onSurfaceVariantDark = Color(0xFFCDC6B4)
+val outlineDark = Color(0xFF969080)
+val outlineVariantDark = Color(0xFF4B4739)
+val scrimDark = Color(0xFF000000)
+val inverseSurfaceDark = Color(0xFFE8E2D4)
+val inverseOnSurfaceDark = Color(0xFF333027)
+val inversePrimaryDark = Color(0xFF6D5E0F)
+val surfaceDimDark = Color(0xFF15130B)
+val surfaceBrightDark = Color(0xFF3C3930)
+val surfaceContainerLowestDark = Color(0xFF100E07)
+val surfaceContainerLowDark = Color(0xFF1E1B13)
+val surfaceContainerDark = Color(0xFF222017)
+val surfaceContainerHighDark = Color(0xFF2D2A21)
+val surfaceContainerHighestDark = Color(0xFF38352B)
+
+val primaryDarkMediumContrast = Color(0xFFF2DC82)
+val onPrimaryDarkMediumContrast = Color(0xFF2D2500)
+val primaryContainerDarkMediumContrast = Color(0xFFA2903F)
+val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
+val secondaryDarkMediumContrast = Color(0xFFE8DCB6)
+val onSecondaryDarkMediumContrast = Color(0xFF2B250C)
+val secondaryContainerDarkMediumContrast = Color(0xFF9A916F)
+val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
+val tertiaryDarkMediumContrast = Color(0xFFBFE6C8)
+val onTertiaryDarkMediumContrast = Color(0xFF082C19)
+val tertiaryContainerDarkMediumContrast = Color(0xFF74997F)
+val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
+val errorDarkMediumContrast = Color(0xFFFFD2CC)
+val onErrorDarkMediumContrast = Color(0xFF540003)
+val errorContainerDarkMediumContrast = Color(0xFFFF5449)
+val onErrorContainerDarkMediumContrast = Color(0xFF000000)
+val backgroundDarkMediumContrast = Color(0xFF15130B)
+val onBackgroundDarkMediumContrast = Color(0xFFE8E2D4)
+val surfaceDarkMediumContrast = Color(0xFF15130B)
+val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
+val surfaceVariantDarkMediumContrast = Color(0xFF4B4739)
+val onSurfaceVariantDarkMediumContrast = Color(0xFFE3DCCA)
+val outlineDarkMediumContrast = Color(0xFFB8B1A0)
+val outlineVariantDarkMediumContrast = Color(0xFF969080)
+val scrimDarkMediumContrast = Color(0xFF000000)
+val inverseSurfaceDarkMediumContrast = Color(0xFFE8E2D4)
+val inverseOnSurfaceDarkMediumContrast = Color(0xFF2D2A21)
+val inversePrimaryDarkMediumContrast = Color(0xFF544700)
+val surfaceDimDarkMediumContrast = Color(0xFF15130B)
+val surfaceBrightDarkMediumContrast = Color(0xFF48443A)
+val surfaceContainerLowestDarkMediumContrast = Color(0xFF090703)
+val surfaceContainerLowDarkMediumContrast = Color(0xFF201D15)
+val surfaceContainerDarkMediumContrast = Color(0xFF2A281F)
+val surfaceContainerHighDarkMediumContrast = Color(0xFF353229)
+val surfaceContainerHighestDarkMediumContrast = Color(0xFF413D34)
+
+val primaryDarkHighContrast = Color(0xFFFFF0B9)
+val onPrimaryDarkHighContrast = Color(0xFF000000)
+val primaryContainerDarkHighContrast = Color(0xFFD7C26B)
+val onPrimaryContainerDarkHighContrast = Color(0xFF0F0B00)
+val secondaryDarkHighContrast = Color(0xFFFCF0C9)
+val onSecondaryDarkHighContrast = Color(0xFF000000)
+val secondaryContainerDarkHighContrast = Color(0xFFCDC29E)
+val onSecondaryContainerDarkHighContrast = Color(0xFF0F0B00)
+val tertiaryDarkHighContrast = Color(0xFFD2FADB)
+val onTertiaryDarkHighContrast = Color(0xFF000000)
+val tertiaryContainerDarkHighContrast = Color(0xFFA5CCAF)
+val onTertiaryContainerDarkHighContrast = Color(0xFF000F05)
+val errorDarkHighContrast = Color(0xFFFFECE9)
+val onErrorDarkHighContrast = Color(0xFF000000)
+val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
+val onErrorContainerDarkHighContrast = Color(0xFF220001)
+val backgroundDarkHighContrast = Color(0xFF15130B)
+val onBackgroundDarkHighContrast = Color(0xFFE8E2D4)
+val surfaceDarkHighContrast = Color(0xFF15130B)
+val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
+val surfaceVariantDarkHighContrast = Color(0xFF4B4739)
+val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
+val outlineDarkHighContrast = Color(0xFFF7EFDD)
+val outlineVariantDarkHighContrast = Color(0xFFC9C2B1)
+val scrimDarkHighContrast = Color(0xFF000000)
+val inverseSurfaceDarkHighContrast = Color(0xFFE8E2D4)
+val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
+val inversePrimaryDarkHighContrast = Color(0xFF544700)
+val surfaceDimDarkHighContrast = Color(0xFF15130B)
+val surfaceBrightDarkHighContrast = Color(0xFF535046)
+val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
+val surfaceContainerLowDarkHighContrast = Color(0xFF222017)
+val surfaceContainerDarkHighContrast = Color(0xFF333027)
+val surfaceContainerHighDarkHighContrast = Color(0xFF3E3B32)
+val surfaceContainerHighestDarkHighContrast = Color(0xFF4A473D)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/CreatePlayerCard.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/CreatePlayerCard.kt
new file mode 100644
index 0000000..bbcbb7a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/CreatePlayerCard.kt
@@ -0,0 +1,239 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import hu.mokegyesulet.it.dunestrat.model.Player
+import hu.mokegyesulet.it.dunestrat.model.Student
+import hu.mokegyesulet.it.dunestrat.model.Team
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+import org.jetbrains.compose.ui.tooling.preview.Preview
+
+@Composable
+@Preview
+@OptIn(ExperimentalMaterial3Api::class)
+fun GlobalStartingConditionsCard(
+ modifier: Modifier = Modifier,
+ player: Player,
+ onChange: (Player) -> Unit,
+) {
+ val weaponLabels = mapOf(
+ Weapon.PISTOL to "Pisztoly",
+ Weapon.LASGUN to "Lasgun",
+ Weapon.CRYSKNIFE to "Crysknife",
+ Weapon.LEGION to "Légió",
+ )
+
+ Card(
+ modifier = modifier.wrapContentSize()
+ .padding(20.dp),
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text("Kezdő állapot", style = MaterialTheme.typography.titleMedium)
+ Spacer(modifier = Modifier.height(16.dp))
+
+ NumericTextField(
+ value = player.water,
+ onValueChange = { onChange(player.copy(water = it)) },
+ label = "Víz készlet",
+ )
+
+ NumericTextField(
+ value = player.spice,
+ onValueChange = { onChange(player.copy(spice = it)) },
+ label = "Fűszer készlet",
+ defaultValue = 10,
+ )
+
+ Weapon.entries.forEach { weapon ->
+ NumericTextField(
+ value = player.getWeaponCount(weapon),
+ onValueChange = { newValue ->
+ val newWeapons = Weapon.entries.associateWith {
+ player.getWeaponCount(it)
+ }.toMutableMap()
+ newWeapons[weapon] = newValue
+ onChange(player.copy(weapons = newWeapons))
+ },
+ label = weaponLabels[weapon] ?: weapon.name,
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PlayerStartingFieldCard(
+ index: Int,
+ modifier: Modifier = Modifier,
+ startingFieldId: String,
+ onStartingFieldChange: (String) -> Unit,
+ availableStartingFields: List,
+ isFieldDuplicate: Boolean,
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Card(
+ modifier = modifier.padding(horizontal = 20.dp, vertical = 8.dp),
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text("${index + 1}. játékos kezdő mezője", style = MaterialTheme.typography.labelLarge)
+ Spacer(modifier = Modifier.height(8.dp))
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ OutlinedTextField(
+ value = startingFieldId,
+ onValueChange = {},
+ readOnly = true,
+ label = { Text("Kezdő mező") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
+ },
+ modifier = Modifier.menuAnchor(
+ ExposedDropdownMenuAnchorType.PrimaryNotEditable,
+ ).fillMaxWidth(),
+ isError = isFieldDuplicate || startingFieldId.isBlank(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ availableStartingFields.forEach { fieldId ->
+ DropdownMenuItem(
+ text = { Text(fieldId) },
+ onClick = {
+ onStartingFieldChange(fieldId)
+ expanded = false
+ },
+ )
+ }
+ }
+ }
+
+ if (isFieldDuplicate) {
+ Text(
+ text = "Ez a mező már ki van választva!",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp, top = 6.dp),
+ )
+ } else if (startingFieldId.isBlank()) {
+ Text(
+ text = "Ez a mező kötelező!",
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp, top = 6.dp),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun NumericTextField(
+ value: Int,
+ onValueChange: (Int) -> Unit,
+ label: String,
+ defaultValue: Int = 0,
+) {
+ TextField(
+ value = value.toString(),
+ onValueChange = { input ->
+ val newValue = input.toIntOrNull()
+ if (newValue != null && newValue >= 0) {
+ onValueChange(newValue)
+ } else if (input.isEmpty()) {
+ onValueChange(defaultValue)
+ }
+ },
+ label = { Text(label) },
+ isError = value < 0,
+ )
+}
+
+@Composable
+fun TeamConfigurationCard(
+ teamIndex: Int,
+ team: Team,
+ onAddStudent: () -> Unit,
+ onRemoveStudent: (Int) -> Unit,
+ onUpdateStudent: (Int, Student) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier.padding(20.dp),
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text(
+ text = "${teamIndex + 1}. csapat tagjai",
+ style = MaterialTheme.typography.titleMedium,
+ )
+ IconButton(onClick = onAddStudent) {
+ Icon(Icons.Default.Add, contentDescription = "Diák hozzáadása")
+ }
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ team.students.forEachIndexed { studentIndex, student ->
+ Column(modifier = Modifier.padding(vertical = 8.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column(modifier = Modifier.weight(1f)) {
+ OutlinedTextField(
+ value = student.name,
+ onValueChange = {
+ onUpdateStudent(studentIndex, student.copy(name = it))
+ },
+ label = { Text("Név") },
+ isError = student.name.isBlank(),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ if (student.name.isBlank()) {
+ Text(
+ text = "A név nem lehet üres!", // TODO
+ color = MaterialTheme.colorScheme.error,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(start = 16.dp, top = 6.dp),
+ )
+ }
+ Spacer(modifier = Modifier.height(4.dp))
+ OutlinedTextField(
+ value = student.batkabankId,
+ onValueChange = {
+ onUpdateStudent(studentIndex, student.copy(batkabankId = it))
+ },
+ label = { Text("BatkaBank ID (opcionális)") },
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
+ if (team.students.size > 1) {
+ IconButton(onClick = { onRemoveStudent(studentIndex) }) {
+ Icon(Icons.Default.Delete, contentDescription = "Diák eltávolítása")
+ }
+ }
+ }
+ if (studentIndex < team.students.size - 1) {
+ HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Font.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Font.kt
new file mode 100644
index 0000000..fbfa815
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Font.kt
@@ -0,0 +1,50 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import dune_strat.composeapp.generated.resources.Dune_Rise
+import dune_strat.composeapp.generated.resources.Res
+import dune_strat.composeapp.generated.resources.metropolis_bold
+import dune_strat.composeapp.generated.resources.metropolis_light
+import dune_strat.composeapp.generated.resources.metropolis_medium
+import dune_strat.composeapp.generated.resources.metropolis_normal
+import dune_strat.composeapp.generated.resources.metropolis_semi_bold
+import org.jetbrains.compose.resources.Font
+
+@Composable
+fun metropolisFontFamily() = FontFamily(
+ Font(Res.font.metropolis_light, weight = FontWeight.Light),
+ Font(Res.font.metropolis_normal, weight = FontWeight.Normal),
+ Font(Res.font.metropolis_medium, weight = FontWeight.Medium),
+ Font(Res.font.metropolis_semi_bold, weight = FontWeight.SemiBold),
+ Font(Res.font.metropolis_bold, weight = FontWeight.Bold),
+)
+
+@Composable
+fun duneFontFamily() = FontFamily(
+ Font(Res.font.Dune_Rise),
+)
+
+@Composable
+fun metropolisTypography() = Typography().run {
+ val fontFamily = metropolisFontFamily()
+ copy(
+ displayLarge = displayLarge.copy(fontFamily = fontFamily),
+ displayMedium = displayMedium.copy(fontFamily = fontFamily),
+ displaySmall = displaySmall.copy(fontFamily = fontFamily),
+ headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
+ headlineMedium = headlineMedium.copy(fontFamily = fontFamily),
+ headlineSmall = headlineSmall.copy(fontFamily = fontFamily),
+ titleLarge = titleLarge.copy(fontFamily = fontFamily),
+ titleMedium = titleMedium.copy(fontFamily = fontFamily),
+ titleSmall = titleSmall.copy(fontFamily = fontFamily),
+ bodyLarge = bodyLarge.copy(fontFamily = fontFamily),
+ bodyMedium = bodyMedium.copy(fontFamily = fontFamily),
+ bodySmall = bodySmall.copy(fontFamily = fontFamily),
+ labelLarge = labelLarge.copy(fontFamily = fontFamily),
+ labelMedium = labelMedium.copy(fontFamily = fontFamily),
+ labelSmall = labelSmall.copy(fontFamily = fontFamily),
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Shapes.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Shapes.kt
new file mode 100644
index 0000000..229956c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Shapes.kt
@@ -0,0 +1,42 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.foundation.shape.GenericShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.ui.unit.dp
+
+val hexagonalRectangle = GenericShape { size, _ ->
+ val width = size.width
+ val height = size.height
+
+ lineTo(0f, height * 0.9f)
+ lineTo(width / 2, height)
+ lineTo(width, height * 0.9f)
+ lineTo(width, height * 0.1f)
+ lineTo(width / 2, 0f)
+ lineTo(0f, height * 0.1f)
+}
+
+val fancyRectangle = GenericShape { size, _ ->
+ val width = size.width
+ val height = size.height
+ val fancyBitSize = 5f
+
+ lineTo(0f, height)
+ lineTo(width / 2 - fancyBitSize, height)
+ lineTo(width / 2, height + fancyBitSize)
+ lineTo(width / 2 + fancyBitSize, height)
+ lineTo(width, height)
+ lineTo(width, 0f)
+ lineTo(width / 2 + fancyBitSize, 0f)
+ lineTo(width / 2, -fancyBitSize)
+ lineTo(width / 2 - fancyBitSize, 0f)
+ lineTo(0f, 0f)
+}
+val shapes = Shapes(
+ extraSmall = RoundedCornerShape(4.dp),
+ small = RoundedCornerShape(8.dp),
+ medium = RoundedCornerShape(16.dp),
+ large = RoundedCornerShape(24.dp),
+ extraLarge = RoundedCornerShape(32.dp),
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/TabKeyNavigable.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/TabKeyNavigable.kt
new file mode 100644
index 0000000..cfef7d4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/TabKeyNavigable.kt
@@ -0,0 +1,26 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.isShiftPressed
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.key.type
+
+fun Modifier.tabKeyNavigable(focusManager: FocusManager): Modifier = this.onPreviewKeyEvent {
+ if (it.type == KeyEventType.KeyDown && it.key == Key.Tab) {
+ focusManager.moveFocus(
+ if (it.isShiftPressed) {
+ FocusDirection.Previous
+ } else {
+ FocusDirection.Next
+ },
+ )
+ true
+ } else {
+ false
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Theme.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Theme.kt
new file mode 100644
index 0000000..3e254db
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Theme.kt
@@ -0,0 +1,102 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+
+private val lightScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ primaryContainer = primaryContainerLight,
+ onPrimaryContainer = onPrimaryContainerLight,
+ secondary = secondaryLight,
+ onSecondary = onSecondaryLight,
+ secondaryContainer = secondaryContainerLight,
+ onSecondaryContainer = onSecondaryContainerLight,
+ tertiary = tertiaryLight,
+ onTertiary = onTertiaryLight,
+ tertiaryContainer = tertiaryContainerLight,
+ onTertiaryContainer = onTertiaryContainerLight,
+ error = errorLight,
+ onError = onErrorLight,
+ errorContainer = errorContainerLight,
+ onErrorContainer = onErrorContainerLight,
+ background = backgroundLight,
+ onBackground = onBackgroundLight,
+ surface = surfaceLight,
+ onSurface = onSurfaceLight,
+ surfaceVariant = surfaceVariantLight,
+ onSurfaceVariant = onSurfaceVariantLight,
+ outline = outlineLight,
+ outlineVariant = outlineVariantLight,
+ scrim = scrimLight,
+ inverseSurface = inverseSurfaceLight,
+ inverseOnSurface = inverseOnSurfaceLight,
+ inversePrimary = inversePrimaryLight,
+ surfaceDim = surfaceDimLight,
+ surfaceBright = surfaceBrightLight,
+ surfaceContainerLowest = surfaceContainerLowestLight,
+ surfaceContainerLow = surfaceContainerLowLight,
+ surfaceContainer = surfaceContainerLight,
+ surfaceContainerHigh = surfaceContainerHighLight,
+ surfaceContainerHighest = surfaceContainerHighestLight,
+)
+
+private val darkScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ onTertiaryContainer = onTertiaryContainerDark,
+ error = errorDark,
+ onError = onErrorDark,
+ errorContainer = errorContainerDark,
+ onErrorContainer = onErrorContainerDark,
+ background = backgroundDark,
+ onBackground = onBackgroundDark,
+ surface = surfaceDark,
+ onSurface = onSurfaceDark,
+ surfaceVariant = surfaceVariantDark,
+ onSurfaceVariant = onSurfaceVariantDark,
+ outline = outlineDark,
+ outlineVariant = outlineVariantDark,
+ scrim = scrimDark,
+ inverseSurface = inverseSurfaceDark,
+ inverseOnSurface = inverseOnSurfaceDark,
+ inversePrimary = inversePrimaryDark,
+ surfaceDim = surfaceDimDark,
+ surfaceBright = surfaceBrightDark,
+ surfaceContainerLowest = surfaceContainerLowestDark,
+ surfaceContainerLow = surfaceContainerLowDark,
+ surfaceContainer = surfaceContainerDark,
+ surfaceContainerHigh = surfaceContainerHighDark,
+ surfaceContainerHighest = surfaceContainerHighestDark,
+)
+
+@Composable
+fun AppTheme(
+ useDarkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit,
+) {
+ val colors = if (!useDarkTheme) {
+ lightScheme
+ } else {
+ darkScheme
+ }
+
+ MaterialTheme(
+ typography = metropolisTypography(),
+ colorScheme = colors,
+ content = content,
+ shapes = shapes,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Type.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Type.kt
new file mode 100644
index 0000000..e68ba9c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/Type.kt
@@ -0,0 +1,39 @@
+package hu.mokegyesulet.it.dunestrat.ui
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+val typography = Typography(
+ headlineSmall = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp,
+ ),
+ titleLarge = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 18.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp,
+ ),
+ bodyLarge = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp,
+ ),
+ bodyMedium = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp,
+ ),
+ labelMedium = TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp,
+ ),
+)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/EnterStepsCard.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/EnterStepsCard.kt
new file mode 100644
index 0000000..49a1ae5
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/EnterStepsCard.kt
@@ -0,0 +1,61 @@
+package hu.mokegyesulet.it.dunestrat.ui.step
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.Card
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+
+@Composable
+fun EnterStepsCard(
+ stepCardState: StepCardState,
+ onStepCardStateChanged: (StepCardEvent) -> Unit,
+) {
+ Card {
+ Column {
+ Text("Player: ${stepCardState.playerId}")
+ Row {
+ Text("Lelép:")
+ TextField(
+ value = stepCardState.leaveFields,
+ onValueChange = {
+ onStepCardStateChanged(StepCardEvent.LeaveFieldsChanged(it))
+ },
+ )
+ }
+ Row {
+ Text("Rálép:")
+ TextField(
+ value = stepCardState.enterFields,
+ onValueChange = {
+ onStepCardStateChanged(StepCardEvent.EnterFieldsChanged(it))
+ },
+ )
+ }
+ Row {
+ Text("Fegyver:")
+ for (w in Weapon.entries) {
+ TextField(
+ value = stepCardState.purchaseWeapons[w].toString(),
+ onValueChange = {
+ onStepCardStateChanged(
+ StepCardEvent.PurchaseWeaponsChanged(w, it.toInt()),
+ )
+ },
+ )
+ }
+ }
+ Row {
+ Text("Harvester:")
+ TextField(
+ value = stepCardState.buildHarvesters,
+ onValueChange = {
+ onStepCardStateChanged(StepCardEvent.BuildHarvestersChanged(it))
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/StepCardState.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/StepCardState.kt
new file mode 100644
index 0000000..431799a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/ui/step/StepCardState.kt
@@ -0,0 +1,22 @@
+package hu.mokegyesulet.it.dunestrat.ui.step
+
+import hu.mokegyesulet.it.dunestrat.model.Weapon
+
+data class StepCardState(
+ val playerId: Int,
+ val leaveFields: String = "",
+ val enterFields: String = "",
+ val purchaseWeapons: Map = mapOf(
+ Weapon.LASGUN to 0,
+ Weapon.PISTOL to 0,
+ Weapon.CRYSKNIFE to 0,
+ Weapon.LEGION to 0,
+ ),
+ val buildHarvesters: String = "",
+)
+sealed class StepCardEvent {
+ class LeaveFieldsChanged(val leaveFields: String) : StepCardEvent()
+ class EnterFieldsChanged(val enterFields: String) : StepCardEvent()
+ class PurchaseWeaponsChanged(val weaponType: Weapon, val amount: Int) : StepCardEvent()
+ class BuildHarvestersChanged(val buildHarvesters: String) : StepCardEvent()
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/StringNormalizer.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/StringNormalizer.kt
new file mode 100644
index 0000000..4606e1d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/StringNormalizer.kt
@@ -0,0 +1,37 @@
+package hu.mokegyesulet.it.dunestrat.util
+
+object StringNormalizer {
+ /**
+ * Normalizes a string by:
+ * - Removing accents from characters (á -> a, é -> e, etc.)
+ * - Converting to lowercase
+ * - Replacing spaces with underscores
+ */
+ fun normalize(text: String): String = removeAccents(text)
+ .lowercase()
+ .replace(" ", "_")
+
+ /**
+ * Removes accents from a string (á -> a, é -> e, etc.)
+ * Useful when you only need accent removal without other transformations.
+ */
+ fun removeAccents(text: String): String {
+ val accentMap = mapOf(
+ 'á' to 'a', 'à' to 'a', 'ä' to 'a', 'â' to 'a', 'ã' to 'a', 'å' to 'a',
+ 'é' to 'e', 'è' to 'e', 'ë' to 'e', 'ê' to 'e',
+ 'í' to 'i', 'ì' to 'i', 'ï' to 'i', 'î' to 'i',
+ 'ó' to 'o', 'ò' to 'o', 'ö' to 'o', 'ô' to 'o', 'õ' to 'o',
+ 'ú' to 'u', 'ù' to 'u', 'ü' to 'u', 'û' to 'u',
+ 'ý' to 'y', 'ỳ' to 'y', 'ÿ' to 'y',
+ 'ç' to 'c', 'ñ' to 'n',
+ 'Á' to 'A', 'À' to 'A', 'Ä' to 'A', 'Â' to 'A', 'Ã' to 'A', 'Å' to 'A',
+ 'É' to 'E', 'È' to 'E', 'Ë' to 'E', 'Ê' to 'E',
+ 'Í' to 'I', 'Ì' to 'I', 'Ï' to 'I', 'Î' to 'I',
+ 'Ó' to 'O', 'Ò' to 'O', 'Ö' to 'O', 'Ô' to 'O', 'Õ' to 'O',
+ 'Ú' to 'U', 'Ù' to 'U', 'Ü' to 'U', 'Û' to 'U',
+ 'Ý' to 'Y',
+ 'Ç' to 'C', 'Ñ' to 'N',
+ )
+ return text.map { accentMap[it] ?: it }.joinToString("")
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.kt
new file mode 100644
index 0000000..2b18f9e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.kt
@@ -0,0 +1,3 @@
+package hu.mokegyesulet.it.dunestrat.util
+
+expect fun openURL(url: String)
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/MapDrawer.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/MapDrawer.kt
new file mode 100644
index 0000000..cf0acdc
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/MapDrawer.kt
@@ -0,0 +1,504 @@
+package hu.mokegyesulet.it.dunestrat.util.drawmap
+
+import dune_strat.composeapp.generated.resources.Res
+import hu.mokegyesulet.it.dunestrat.model.*
+import kotlin.math.PI
+
+object MapDrawer {
+
+ private const val HEXAGON_SIZE = 200.0
+ private const val INNER_HEXAGON_RATIO = 0.65
+
+ private const val POLYGON_STYLE = "style=\"fill:none;stroke:#000000;stroke-width:2.5px\""
+ private const val BOX_STYLE =
+ "style=\"fill:#DDDDDD;fill-opacity:0.8;stroke:#000000;stroke-width:2.5px\""
+
+ private val distanceToVertex = Vector2D(HEXAGON_SIZE / 2, 0.0)
+
+ private val hexagonVertices = (0..5).map {
+ distanceToVertex.rotate(PI / 3 * it)
+ }
+
+ private val innerHexagonVertices = hexagonVertices.map { it * INNER_HEXAGON_RATIO }
+
+ private val numberOffsetVector = hexagonVertices[1] - hexagonVertices[5]
+ private val letterOffsetVector2D = (hexagonVertices[0] - hexagonVertices[4]).invertY()
+
+ private lateinit var desertSVG: String
+ private lateinit var mountainSVG: String
+
+ private lateinit var pistolSVG: String
+ private lateinit var lasgunSVG: String
+ private lateinit var crysknifeSVG: String
+
+ private lateinit var waterSVG: String
+ private lateinit var spiceSVG: String
+
+ private var resourcesLoaded = false
+
+ suspend fun loadResources(blackAndWhite: Boolean = false) {
+ if (resourcesLoaded) {
+ return
+ }
+
+ val desertBytes = if (blackAndWhite) {
+ Res.readBytes("drawable/svg/desert_bnw.svg")
+ } else {
+ Res.readBytes("drawable/svg/desert.svg")
+ }
+ desertSVG = desertBytes.decodeToString().replace("cls-", "desert-cls-") + "\n"
+
+ val mountainBytes = if (blackAndWhite) {
+ Res.readBytes("drawable/svg/mountain_bnw.svg")
+ } else {
+ Res.readBytes("drawable/svg/mountain.svg")
+ }
+ mountainSVG = mountainBytes.decodeToString().replace("cls-", "mountain-cls-") + "\n"
+
+ val pistolBytes = Res.readBytes("drawable/svg/pistol.svg")
+ pistolSVG = pistolBytes.decodeToString().replace("cls-", "pistol-cls-") + "\n"
+
+ val lasgunBytes = Res.readBytes("drawable/svg/lasgun.svg")
+ lasgunSVG = lasgunBytes.decodeToString().replace("cls-", "lasgun-cls-") + "\n"
+
+ val crysknifeBytes = Res.readBytes("drawable/svg/crysknife.svg")
+ crysknifeSVG = crysknifeBytes.decodeToString().replace("cls-", "crysknife-cls-") + "\n"
+
+ val waterBytes = Res.readBytes("drawable/svg/water.svg")
+ waterSVG = waterBytes.decodeToString().replace("cls-", "water-cls-") + "\n"
+
+ val spiceBytes = Res.readBytes("drawable/svg/spice.svg")
+ spiceSVG = spiceBytes.decodeToString().replace("cls-", "spice-cls-") + "\n"
+
+ resourcesLoaded = true
+ }
+
+ private fun getDefinitions(): String {
+ val builder = StringBuilder()
+ builder.append("\n")
+
+ builder.append("\n")
+ builder.append(desertSVG)
+ builder.append("\n")
+
+ builder.append("\n")
+ builder.append(mountainSVG)
+ builder.append("\n")
+
+ builder.append("\n")
+ builder.append(pistolSVG)
+ builder.append("\n")
+
+ builder.append("\n")
+ builder.append(lasgunSVG)
+ builder.append("\n")
+
+ builder.append("\n")
+ builder.append(crysknifeSVG)
+ builder.append("\n")
+
+ builder.append("\n")
+ return builder.toString()
+ }
+
+ private fun getOffsetVector(coordinate: String): Vector2D {
+ val letterOffset = coordinate.lowercase()[0].code - 'a'.code
+ val numberOffset = coordinate.substring(1).toInt() - 1
+ return letterOffsetVector2D * letterOffset + numberOffsetVector * numberOffset
+ }
+
+ private fun getHexagonSvg(offset: Vector2D): String {
+ val points = hexagonVertices.map { it + offset }
+ return polygonSvgFromPoints(points)
+ }
+
+ private fun getFieldTextSvg(
+ fieldId: String,
+ offset: Vector2D,
+ ): String {
+ val center =
+ ((hexagonVertices[4] + hexagonVertices[5]) / 2) + Vector2D(0.0, 20.0) + offset
+ val position = "x=\"${center.x}\" y=\"${center.y}\""
+ val font = "font-size=\"20\" font-family=\"Times New Roman\""
+ val align = "dominant-baseline=\"middle\" text-anchor=\"middle\""
+ return "$fieldId\n"
+ }
+
+ private fun polygonSvgFromPoints(points: List): String {
+ val pointsString = points.joinToString(separator = " ") { "${it.x},${it.y}" }
+ return "\n"
+ }
+
+ private fun boxSvgFromPoints(points: List): String {
+ val pointsString = points.joinToString(separator = " ") { "${it.x},${it.y}" }
+ val style = "style=\"fill:#DDDDDD;fill-opacity:0.8;stroke:#000000;stroke-width:2.5px\""
+ return "\n"
+ }
+
+ private fun getWeaponBoxSvg(offset: Vector2D): String {
+ val points = listOf(
+ hexagonVertices[1],
+ innerHexagonVertices[1],
+ innerHexagonVertices[2],
+ hexagonVertices[2],
+ ).map { it + offset }
+ return boxSvgFromPoints(points)
+ }
+
+ private fun getWaterBoxSvg(offset: Vector2D): String {
+ val points = listOf(
+ hexagonVertices[2],
+ innerHexagonVertices[2],
+ innerHexagonVertices[3],
+ hexagonVertices[3],
+ ).map { it + offset }
+ return boxSvgFromPoints(points)
+ }
+
+ private fun getSpiceBoxSvg(offset: Vector2D): String {
+ val points = listOf(
+ hexagonVertices[0],
+ innerHexagonVertices[0],
+ innerHexagonVertices[1],
+ hexagonVertices[1],
+ ).map { it + offset }
+ return boxSvgFromPoints(points)
+ }
+
+ private fun getCoordinateCircleSvg(offset: Vector2D): String {
+ val center = (hexagonVertices[4] + hexagonVertices[5]) / 2 + offset
+ val radius = (HEXAGON_SIZE * 3) / 16
+ val d = "M ${center.x - radius} ${center.y} " +
+ "A $radius $radius 0 0 0 " +
+ "${center.x + radius} ${center.y}"
+ return "\n"
+ }
+
+ private val backgroundOffset =
+ hexagonVertices[3] - hexagonVertices[2] + (hexagonVertices[2] - hexagonVertices[1]) / 2
+
+ private fun getBackgroundSvg(
+ desert: Boolean,
+ offset: Vector2D,
+ ): String {
+ val fullOffset = offset + backgroundOffset
+ if (desert) {
+ return "\n"
+ }
+ return "\n"
+ }
+
+ private fun getWaterSvg(
+ value: Int,
+ offset: Vector2D,
+ ): String {
+ val midpoint = (hexagonVertices[1] + innerHexagonVertices[2]) / 2
+ val totalOffset = offset + midpoint
+
+ val font = "font-size=\"20\" font-family=\"Times New Roman\""
+ val align = "dominant-baseline=\"middle\" text-anchor=\"middle\""
+
+ val rotate = "rotate(60,${offset.x},${offset.y})"
+ val translate = "translate(${totalOffset.x}, ${totalOffset.y})"
+ val transform = "transform=\"$rotate $translate\""
+
+ val svg = StringBuilder()
+ svg.append("\n")
+ svg.append("\n")
+ svg.append(waterSVG)
+ svg.append("\n")
+ svg.append("$value\n")
+ svg.append("\n")
+
+ return svg.toString()
+ }
+
+ private fun getSpiceSvg(
+ value: Int,
+ offset: Vector2D,
+ isRed: Boolean = false,
+ ): String {
+ val midpoint = (hexagonVertices[1] + innerHexagonVertices[2]) / 2
+ val totalOffset = offset + midpoint
+
+ val font = "font-size=\"20\" font-family=\"Times New Roman\""
+ val align = "dominant-baseline=\"middle\" text-anchor=\"middle\""
+ val color = if (isRed) "red" else "black"
+
+ val rotate = "rotate(-60,${offset.x},${offset.y})"
+ val translate = "translate(${totalOffset.x}, ${totalOffset.y})"
+ val transform = "transform=\"$rotate $translate\""
+
+ val svg = StringBuilder()
+ svg.append("\n")
+ svg.append("\n")
+ svg.append(spiceSVG)
+ svg.append("\n")
+ svg.append("$value\n")
+ svg.append("\n")
+
+ return svg.toString()
+ }
+
+ private val weaponOffset = innerHexagonVertices[2]
+
+ private fun getWeaponSvg(
+ weapon: Weapon,
+ offset: Vector2D,
+ ): String {
+ var totalOffset = offset + weaponOffset
+ return when (weapon) {
+ Weapon.PISTOL -> {
+ "\n"
+ }
+
+ Weapon.LASGUN -> {
+ "\n"
+ }
+
+ Weapon.CRYSKNIFE -> {
+ totalOffset += Vector2D(0.0, 3.0)
+ "\n"
+ }
+
+ Weapon.LEGION -> {
+ ""
+ }
+ }
+ }
+
+ private fun getPlayerMarkerSvg(
+ offset: Vector2D,
+ text: String,
+ circleColor: String,
+ textColor: String,
+ ): String {
+ val textPosition = "x=\"${offset.x}\" y=\"${offset.y}\""
+ val circlePosition = "cx=\"${offset.x}\" cy=\"${offset.y}\""
+ val align = "dominant-baseline=\"middle\" text-anchor=\"middle\""
+ val font = "font-size=\"30\""
+
+ val circle = "\n"
+ val text = "$text\n"
+ return circle + text
+ }
+
+ fun getDesertSvg(desert: Desert): String {
+ var minX = 0.0
+ var maxX = 0.0
+ var minY = 0.0
+ var maxY = 0.0
+
+ val fieldSvgs = desert.fields.map { field ->
+ val offset = getOffsetVector(field.id)
+
+ minX = minOf(minX, offset.x)
+ maxX = maxOf(maxX, offset.x)
+ minY = minOf(minY, offset.y)
+ maxY = maxOf(maxY, offset.y)
+
+ val fieldBuilder = StringBuilder()
+
+ fieldBuilder.append(getBackgroundSvg(field.water < 0, offset))
+
+ fieldBuilder.append(getHexagonSvg(offset))
+ fieldBuilder.append(getWeaponBoxSvg(offset))
+ fieldBuilder.append(getCoordinateCircleSvg(offset))
+ fieldBuilder.append(getFieldTextSvg(field.id, offset))
+ fieldBuilder.append(getWaterBoxSvg(offset))
+ fieldBuilder.append(getSpiceBoxSvg(offset))
+ fieldBuilder.append(getWaterSvg(field.water, offset))
+ fieldBuilder.append(getSpiceSvg(field.spice, offset))
+ fieldBuilder.append(getWeaponSvg(field.effectiveWeapon, offset))
+
+ fieldBuilder.toString()
+ }
+
+ val width = maxX - minX + 300
+ val height = maxY - minY + 300
+
+ val viewbox = "viewBox=\"${minX - 150} ${minY - 150} $width $height\""
+
+ val svgOpening = "\n"
+
+ val definitions = getDefinitions()
+
+ val svgEnd = ""
+ return svgOpening + definitions + fieldSvgs.joinToString(separator = "") + svgEnd
+ }
+
+ private const val GREEK_ALPHABET = "αβγδεζηθικλμνξοπρστυφχψω"
+ private val COLORS = listOf(
+ "darkorange" to "white",
+ "cyan" to "black",
+ "mediumseagreen" to "white",
+ "gold" to "white",
+ "deeppink" to "white",
+ "white" to "black",
+ "slateblue" to "white",
+ "darkcyan" to "white",
+ "crimson" to "white",
+ "peru" to "white",
+ "black" to "white",
+ "darkorchid" to "white",
+ )
+
+ fun getGameStateSvg(gameState: GameState): String {
+ var minX = 0.0
+ var maxX = 0.0
+ var minY = 0.0
+ var maxY = 0.0
+
+ val fieldSvgs = gameState.fields.map { field ->
+ val offset = getOffsetVector(field.id)
+
+ minX = minOf(minX, offset.x)
+ maxX = maxOf(maxX, offset.x)
+ minY = minOf(minY, offset.y)
+ maxY = maxOf(maxY, offset.y)
+
+ val fieldBuilder = StringBuilder()
+
+ fieldBuilder.append(getBackgroundSvg(field.water < 0, offset))
+
+ fieldBuilder.append(getHexagonSvg(offset))
+ fieldBuilder.append(getWeaponBoxSvg(offset))
+ fieldBuilder.append(getCoordinateCircleSvg(offset))
+ fieldBuilder.append(getFieldTextSvg(field.id, offset))
+ fieldBuilder.append(getWaterBoxSvg(offset))
+ fieldBuilder.append(getSpiceBoxSvg(offset))
+ fieldBuilder.append(getWaterSvg(field.water, offset))
+ fieldBuilder.append(getSpiceSvg(field.spice, offset, field.harvester))
+ fieldBuilder.append(getWeaponSvg(field.effectiveWeapon, offset))
+
+ fieldBuilder.toString()
+ }
+
+ val playerSvgs = gameState.players.map { player ->
+ val ownedFieldsSvgs = player.ownedFields.map { field ->
+ val offset = getOffsetVector(field.id)
+ getPlayerMarkerSvg(
+ offset,
+ GREEK_ALPHABET[player.id].toString(),
+ COLORS[player.id].first,
+ COLORS[player.id].second,
+ )
+ }
+ ownedFieldsSvgs.joinToString(separator = "")
+ }
+
+ val width = maxX - minX + 300
+ val height = maxY - minY + 300
+
+ val viewbox = "viewBox=\"${minX - 150} ${minY - 150} $width $height\""
+
+ val svgOpening = "\n"
+
+ val definitions = getDefinitions()
+
+ val svgEnd = ""
+ return svgOpening + definitions + fieldSvgs.joinToString(separator = "") +
+ playerSvgs.joinToString(separator = "") +
+ svgEnd
+ }
+}
+
+suspend fun main() {
+// val desert = Desert.create12PlayerHexagon()
+
+ val a1 = GameStateField(
+ id = "A1",
+ water = -3,
+ spice = 28,
+ effectiveWeapon = Weapon.PISTOL,
+ neighbours = mutableSetOf(),
+ harvester = true,
+ )
+ val a2 = GameStateField(
+ id = "A2",
+ water = 7,
+ spice = 7,
+ effectiveWeapon = Weapon.CRYSKNIFE,
+ neighbours = mutableSetOf(),
+ harvester = false,
+ )
+ val b1 = GameStateField(
+ id = "B1",
+ water = -10,
+ spice = 0,
+ effectiveWeapon = Weapon.LASGUN,
+ neighbours = mutableSetOf(),
+ harvester = false,
+ )
+ val b2 = GameStateField(
+ id = "B2",
+ water = 11,
+ spice = 11,
+ effectiveWeapon = Weapon.CRYSKNIFE,
+ neighbours = mutableSetOf(),
+ harvester = false,
+ )
+
+ val gameState = GameState(
+ fields = setOf(
+ a1,
+ a2,
+ b1,
+ b2,
+ ),
+ players = setOf(
+ Player(
+ id = 0,
+ water = 12,
+ spice = 12,
+ harvestersPurchased = 1,
+ weapons = mutableMapOf(),
+ ownedFields = mutableSetOf(a1, a2),
+ ),
+ Player(
+ id = 1,
+ water = 12,
+ spice = 12,
+ harvestersPurchased = 1,
+ weapons = mutableMapOf(),
+ ownedFields = mutableSetOf(b1, b2),
+ ),
+ ),
+ id = -1,
+ gameId = -1,
+ index = -1,
+ )
+
+ val fields = (1..12).map {
+ GameStateField(
+ id = (('A'.code + (it - 1) / 3).toChar().toString() + ((it - 1) % 3 + 1).toString()),
+ 0,
+ 0,
+ Weapon.PISTOL,
+ false,
+ mutableSetOf(),
+ )
+ }
+ val gameState2 = GameState(
+ fields = fields.toMutableSet(),
+ players = fields.mapIndexed { index, field ->
+ Player(
+ id = index,
+ water = 12,
+ spice = 12,
+ harvestersPurchased = 1,
+ weapons = mutableMapOf(),
+ ownedFields = mutableSetOf(field),
+ )
+ }.toMutableSet(),
+ id = -1,
+ gameId = -1,
+ index = -1,
+ )
+
+ MapDrawer.loadResources()
+
+ println(MapDrawer.getGameStateSvg(gameState2))
+}
diff --git a/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/Vector2D.kt b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/Vector2D.kt
new file mode 100644
index 0000000..c820d2e
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/hu/mokegyesulet/it/dunestrat/util/drawmap/Vector2D.kt
@@ -0,0 +1,33 @@
+package hu.mokegyesulet.it.dunestrat.util.drawmap
+
+import kotlin.math.cos
+import kotlin.math.sin
+
+data class Vector2D(
+ val x: Double,
+ val y: Double,
+) {
+ operator fun times(scalar: Double): Vector2D = Vector2D(x * scalar, y * scalar)
+ operator fun times(scalar: Int): Vector2D = this * scalar.toDouble()
+
+ operator fun plus(other: Vector2D): Vector2D = Vector2D(x + other.x, y + other.y)
+
+ operator fun minus(other: Vector2D): Vector2D = Vector2D(x - other.x, y - other.y)
+
+ operator fun div(scalar: Double): Vector2D = this * (1 / scalar)
+ operator fun div(scalar: Int): Vector2D = this / scalar.toDouble()
+
+ fun rotate(angle: Double): Vector2D {
+ val cosAngle = cos(angle)
+ val sinAngle = sin(angle)
+ return Vector2D(
+ x * cosAngle - y * sinAngle,
+ x * sinAngle + y * cosAngle,
+ )
+ }
+
+ fun invertY(): Vector2D = Vector2D(x, -y)
+}
+
+operator fun Double.times(vector: Vector2D): Vector2D = vector * this
+operator fun Int.times(vector: Vector2D): Vector2D = vector * this
diff --git a/composeApp/src/iosMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.ios.kt b/composeApp/src/iosMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.ios.kt
index 0687b0c..de97ee5 100644
--- a/composeApp/src/iosMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.ios.kt
+++ b/composeApp/src/iosMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.ios.kt
@@ -1,10 +1,18 @@
package hu.mokegyesulet.it.dunestrat
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
import platform.UIKit.UIDevice
class IOSPlatform : Platform {
override val name: String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
+ override fun openUrl(url: String) {
+ // Handled natively
+ }
}
actual fun getPlatform(): Platform = IOSPlatform()
+
+@OptIn(SupabaseExperimental::class)
+actual fun getUrlLauncher(): UrlLauncher? = null
diff --git a/composeApp/src/jsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.js.kt b/composeApp/src/jsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.js.kt
index 3eeaac6..803ef49 100644
--- a/composeApp/src/jsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.js.kt
+++ b/composeApp/src/jsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.js.kt
@@ -1,7 +1,16 @@
package hu.mokegyesulet.it.dunestrat
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
+
class JsPlatform : Platform {
override val name: String = "Web with Kotlin/JS"
+ override fun openUrl(url: String) {
+ kotlinx.browser.window.open(url, "_blank")
+ }
}
actual fun getPlatform(): Platform = JsPlatform()
+
+@OptIn(SupabaseExperimental::class)
+actual fun getUrlLauncher(): UrlLauncher? = null
diff --git a/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.jvm.kt b/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.jvm.kt
index 547fc52..4778a42 100644
--- a/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.jvm.kt
+++ b/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.jvm.kt
@@ -1,7 +1,87 @@
package hu.mokegyesulet.it.dunestrat
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
+import java.awt.Desktop
+import java.net.URI
+
class JVMPlatform : Platform {
override val name: String = "Java ${System.getProperty("java.version")}"
+ override fun openUrl(url: String) {
+ val uri = URI(url)
+ try {
+ if (Desktop.isDesktopSupported() &&
+ Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)
+ ) {
+ Desktop.getDesktop().browse(uri)
+ } else {
+ fallbackBrowse(url)
+ }
+ } catch (e: Exception) {
+ fallbackBrowse(url)
+ }
+ }
+
+ private fun fallbackBrowse(url: String) {
+ val os = System.getProperty("os.name").lowercase()
+ if (os.contains("linux")) {
+ val browsers = listOf("xdg-open", "sensible-browser", "google-chrome", "firefox")
+ for (browser in browsers) {
+ try {
+ Runtime.getRuntime().exec(arrayOf(browser, url))
+ return
+ } catch (e: Exception) {
+ // try next
+ }
+ }
+ }
+ println("Please open this URL in your browser: $url")
+ }
}
actual fun getPlatform(): Platform = JVMPlatform()
+
+@OptIn(SupabaseExperimental::class)
+actual fun getUrlLauncher(): UrlLauncher? = UrlLauncher { _, url ->
+ val osName = System.getProperty("os.name").lowercase()
+ val runtime = Runtime.getRuntime()
+ if (osName.contains("win")) {
+ runtime.exec(arrayOf("rundll32", "url.dll,FileProtocolHandler", url))
+ } else if (osName.contains("mac")) {
+ runtime.exec(arrayOf("open", url))
+ } else {
+ // Linux and other Unix-like
+ val browsers =
+ listOf(
+ "xdg-open",
+ "gio open",
+ "gnome-open",
+ "kde-open",
+ "sensible-browser",
+ "firefox",
+ "google-chrome",
+ )
+ var opened = false
+ for (browser in browsers) {
+ try {
+ val browserCmd = browser.split(" ")[0]
+ // Using 'which' to check if command exists
+ if (runtime.exec(arrayOf("which", browserCmd)).waitFor() == 0) {
+ runtime.exec(arrayOf(browserCmd, url))
+ opened = true
+ break
+ }
+ } catch (e: Exception) {
+ // Ignore and try next
+ }
+ }
+ if (!opened) {
+ // Last resort: try just running the first one anyway
+ try {
+ runtime.exec(arrayOf("xdg-open", url))
+ } catch (e: Exception) {
+ // throw UnsupportedOperationException("Could not open browser for URL: $url. Error: ${e.message}")
+ }
+ }
+ }
+}
diff --git a/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.jvm.kt b/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.jvm.kt
new file mode 100644
index 0000000..4b097fe
--- /dev/null
+++ b/composeApp/src/jvmMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.jvm.kt
@@ -0,0 +1,10 @@
+package hu.mokegyesulet.it.dunestrat.util
+
+import java.awt.Desktop
+import java.net.URI
+
+actual fun openURL(url: String) {
+ if (Desktop.isDesktopSupported()) {
+ Desktop.getDesktop().browse(URI(url))
+ }
+}
diff --git a/composeApp/src/nativeMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.native.kt b/composeApp/src/nativeMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.native.kt
new file mode 100644
index 0000000..8c1c66f
--- /dev/null
+++ b/composeApp/src/nativeMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.native.kt
@@ -0,0 +1,9 @@
+package hu.mokegyesulet.it.dunestrat.util
+
+import platform.Foundation.NSURL
+import platform.UIKit.UIApplication
+
+actual fun openURL(url: String) {
+ val nsUrl = NSURL.URLWithString(url) ?: return
+ UIApplication.sharedApplication.openURL(nsUrl)
+}
diff --git a/composeApp/src/wasmJsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.wasmJs.kt
index 2ff504f..bf9ff1b 100644
--- a/composeApp/src/wasmJsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.wasmJs.kt
+++ b/composeApp/src/wasmJsMain/kotlin/hu/mokegyesulet/it/dunestrat/Platform.wasmJs.kt
@@ -1,7 +1,18 @@
package hu.mokegyesulet.it.dunestrat
+import io.github.jan.supabase.annotations.SupabaseExperimental
+import io.github.jan.supabase.auth.UrlLauncher
+
class WasmPlatform : Platform {
override val name: String = "Web with Kotlin/Wasm"
+ override fun openUrl(url: String) {
+ openUrlNative(url)
+ }
}
+private fun openUrlNative(url: String): Unit = js("window.open(url, '_blank')")
+
actual fun getPlatform(): Platform = WasmPlatform()
+
+@OptIn(SupabaseExperimental::class)
+actual fun getUrlLauncher(): UrlLauncher? = null
diff --git a/composeApp/src/webMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.web.kt b/composeApp/src/webMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.web.kt
new file mode 100644
index 0000000..2156735
--- /dev/null
+++ b/composeApp/src/webMain/kotlin/hu/mokegyesulet/it/dunestrat/util/URLOpener.web.kt
@@ -0,0 +1,7 @@
+package hu.mokegyesulet.it.dunestrat.util
+
+import kotlinx.browser.window
+
+actual fun openURL(url: String) {
+ window.open(url, "_blank")
+}
diff --git a/composeApp/src/webMain/resources/favicon.svg b/composeApp/src/webMain/resources/favicon.svg
new file mode 100644
index 0000000..e26abf3
--- /dev/null
+++ b/composeApp/src/webMain/resources/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/composeApp/src/webMain/resources/index.html b/composeApp/src/webMain/resources/index.html
index 07ff436..e180104 100644
--- a/composeApp/src/webMain/resources/index.html
+++ b/composeApp/src/webMain/resources/index.html
@@ -3,7 +3,8 @@
- dune-strat
+ Dűne
+
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..35afeec
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,64 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": [
+ "systems"
+ ]
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1768178648,
+ "narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixpkgs-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs",
+ "systems": "systems"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..837e24a
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,22 @@
+{
+ description = "A basic flake with a shell";
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+ inputs.systems.url = "github:nix-systems/default";
+ inputs.flake-utils = {
+ url = "github:numtide/flake-utils";
+ inputs.systems.follows = "systems";
+ };
+
+ outputs = {
+ nixpkgs,
+ flake-utils,
+ ...
+ }:
+ flake-utils.lib.eachDefaultSystem (
+ system: let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in {
+ devShells.default = pkgs.mkShell {packages = [pkgs.jdk21];};
+ }
+ );
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 50e5c7c..2c934a5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,28 +1,31 @@
[versions]
-agp = "8.12.3"
+agp = "8.13.2"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
-androidx-activity = "1.11.0"
+androidx-activity = "1.12.0"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
-androidx-lifecycle = "2.9.5"
+androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
-composeHotReload = "1.0.0-rc02"
-composeMultiplatform = "1.9.2"
+composeHotReload = "1.0.0"
+composeMultiplatform = "1.9.3"
junit = "4.13.2"
kotlin = "2.2.21"
kotlinx-coroutines = "1.10.2"
-materialIconsExtended = "1.7.8"
-navigationCompose = "2.9.6"
+materialIconsExtended = "1.7.3"
+navigationCompose = "2.9.1"
google-services = "4.4.4"
-firebase = "34.5.0"
-ktlint-plugin = "13.1.0"
+ktlint-plugin = "14.0.1"
+kotlinx-serialization = "1.10.0-RC"
+supabase = "3.3.0"
+ktor = "3.2.2"
+kotlin-serialization = "2.3.0"
[libraries]
-androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
-androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
+navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
@@ -33,10 +36,19 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
-kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
-firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" }
-firebase-auth = { module = "com.google.firebase:firebase-auth" }
-firebase-firestore = { module = "com.google.firebase:firebase-firestore" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+supabase-auth = { module = "io.github.jan-tennert.supabase:auth-kt"}
+supabase-auth-compose = { module = "io.github.jan-tennert.supabase:compose-auth"}
+supabase-postgrest = { module = "io.github.jan-tennert.supabase:postgrest-kt"}
+supabase-storage = { module = "io.github.jan-tennert.supabase:storage-kt"}
+supabase-realtime = { module = "io.github.jan-tennert.supabase:realtime-kt"}
+supabase-bom = { module = "io.github.jan-tennert.supabase:bom", version.ref = "supabase" }
+kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json",version.ref="kotlinx-serialization" }
+ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
+ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
+
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
@@ -47,3 +59,4 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
googleServices = { id = "com.google.gms.google-services", version.ref = "google-services" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-plugin" }
+serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization" }
diff --git a/kotlin-js-store/wasm/yarn.lock b/kotlin-js-store/wasm/yarn.lock
index 5f4567d..d3b9b64 100644
--- a/kotlin-js-store/wasm/yarn.lock
+++ b/kotlin-js-store/wasm/yarn.lock
@@ -6,3 +6,8 @@
version "3.2.0"
resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
+
+ws@8.18.3:
+ version "8.18.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
+ integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock
new file mode 100644
index 0000000..c51ad34
--- /dev/null
+++ b/kotlin-js-store/yarn.lock
@@ -0,0 +1,3050 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@colors/colors@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
+ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
+
+"@discoveryjs/json-ext@^0.6.1":
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz#f13c7c205915eb91ae54c557f5e92bddd8be0e83"
+ integrity sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==
+
+"@isaacs/cliui@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
+ integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+ dependencies:
+ string-width "^5.1.2"
+ string-width-cjs "npm:string-width@^4.2.0"
+ strip-ansi "^7.0.1"
+ strip-ansi-cjs "npm:strip-ansi@^6.0.1"
+ wrap-ansi "^8.1.0"
+ wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.13"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/source-map@^0.3.3":
+ version "0.3.11"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba"
+ integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+
+"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
+ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.31"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0"
+ integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@js-joda/core@3.2.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273"
+ integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg==
+
+"@jsonjoy.com/base64@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578"
+ integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==
+
+"@jsonjoy.com/buffers@^1.0.0", "@jsonjoy.com/buffers@^1.2.0":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz#8d99c7f67eaf724d3428dfd9826c6455266a5c83"
+ integrity sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==
+
+"@jsonjoy.com/codegen@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207"
+ integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==
+
+"@jsonjoy.com/json-pack@^1.11.0":
+ version "1.21.0"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz#93f8dd57fe3a3a92132b33d1eb182dcd9e7629fa"
+ integrity sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==
+ dependencies:
+ "@jsonjoy.com/base64" "^1.1.2"
+ "@jsonjoy.com/buffers" "^1.2.0"
+ "@jsonjoy.com/codegen" "^1.0.0"
+ "@jsonjoy.com/json-pointer" "^1.0.2"
+ "@jsonjoy.com/util" "^1.9.0"
+ hyperdyperid "^1.2.0"
+ thingies "^2.5.0"
+ tree-dump "^1.1.0"
+
+"@jsonjoy.com/json-pointer@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408"
+ integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==
+ dependencies:
+ "@jsonjoy.com/codegen" "^1.0.0"
+ "@jsonjoy.com/util" "^1.9.0"
+
+"@jsonjoy.com/util@^1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46"
+ integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==
+ dependencies:
+ "@jsonjoy.com/buffers" "^1.0.0"
+ "@jsonjoy.com/codegen" "^1.0.0"
+
+"@leichtgewicht/ip-codec@^2.0.1":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
+ integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==
+
+"@pkgjs/parseargs@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
+ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
+
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
+ integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
+
+"@types/body-parser@*":
+ version "1.19.6"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
+ integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
+ dependencies:
+ "@types/connect" "*"
+ "@types/node" "*"
+
+"@types/bonjour@^3.5.13":
+ version "3.5.13"
+ resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956"
+ integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/connect-history-api-fallback@^1.5.4":
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3"
+ integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==
+ dependencies:
+ "@types/express-serve-static-core" "*"
+ "@types/node" "*"
+
+"@types/connect@*":
+ version "3.4.38"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
+ integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
+ dependencies:
+ "@types/node" "*"
+
+"@types/cors@^2.8.12":
+ version "2.8.19"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342"
+ integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/eslint-scope@^3.7.7":
+ version "3.7.7"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
+ integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*":
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584"
+ integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^1.0.8":
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
+ integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b"
+ integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==
+ dependencies:
+ "@types/node" "*"
+ "@types/qs" "*"
+ "@types/range-parser" "*"
+ "@types/send" "*"
+
+"@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33":
+ version "4.19.7"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz#f1d306dcc03b1aafbfb6b4fe684cce8a31cffc10"
+ integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==
+ dependencies:
+ "@types/node" "*"
+ "@types/qs" "*"
+ "@types/range-parser" "*"
+ "@types/send" "*"
+
+"@types/express@*":
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.5.tgz#3ba069177caa34ab96585ca23b3984d752300cdc"
+ integrity sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==
+ dependencies:
+ "@types/body-parser" "*"
+ "@types/express-serve-static-core" "^5.0.0"
+ "@types/serve-static" "^1"
+
+"@types/express@^4.17.21":
+ version "4.17.25"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b"
+ integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==
+ dependencies:
+ "@types/body-parser" "*"
+ "@types/express-serve-static-core" "^4.17.33"
+ "@types/qs" "*"
+ "@types/serve-static" "^1"
+
+"@types/http-errors@*":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
+ integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
+
+"@types/http-proxy@^1.17.8":
+ version "1.17.17"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.17.tgz#d9e2c4571fe3507343cb210cd41790375e59a533"
+ integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==
+ dependencies:
+ "@types/node" "*"
+
+"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.9":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/mime@^1":
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
+ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
+
+"@types/node-forge@^1.3.0":
+ version "1.3.14"
+ resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b"
+ integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*", "@types/node@>=10.0.0":
+ version "24.10.0"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.0.tgz#6b79086b0dfc54e775a34ba8114dcc4e0221f31f"
+ integrity sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==
+ dependencies:
+ undici-types "~7.16.0"
+
+"@types/qs@*":
+ version "6.14.0"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
+ integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
+
+"@types/range-parser@*":
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
+ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
+
+"@types/retry@0.12.2":
+ version "0.12.2"
+ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
+ integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==
+
+"@types/send@*":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74"
+ integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/send@<1":
+ version "0.17.6"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25"
+ integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==
+ dependencies:
+ "@types/mime" "^1"
+ "@types/node" "*"
+
+"@types/serve-index@^1.9.4":
+ version "1.9.4"
+ resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898"
+ integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==
+ dependencies:
+ "@types/express" "*"
+
+"@types/serve-static@^1", "@types/serve-static@^1.15.5":
+ version "1.15.10"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee"
+ integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==
+ dependencies:
+ "@types/http-errors" "*"
+ "@types/node" "*"
+ "@types/send" "<1"
+
+"@types/sockjs@^0.3.36":
+ version "0.3.36"
+ resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535"
+ integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==
+ dependencies:
+ "@types/node" "*"
+
+"@types/ws@^8.5.10":
+ version "8.18.1"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
+ integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
+ dependencies:
+ "@types/node" "*"
+
+"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6"
+ integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==
+ dependencies:
+ "@webassemblyjs/helper-numbers" "1.13.2"
+ "@webassemblyjs/helper-wasm-bytecode" "1.13.2"
+
+"@webassemblyjs/floating-point-hex-parser@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb"
+ integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==
+
+"@webassemblyjs/helper-api-error@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7"
+ integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==
+
+"@webassemblyjs/helper-buffer@1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b"
+ integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==
+
+"@webassemblyjs/helper-numbers@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d"
+ integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser" "1.13.2"
+ "@webassemblyjs/helper-api-error" "1.13.2"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b"
+ integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==
+
+"@webassemblyjs/helper-wasm-section@1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348"
+ integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@webassemblyjs/helper-buffer" "1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.13.2"
+ "@webassemblyjs/wasm-gen" "1.14.1"
+
+"@webassemblyjs/ieee754@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba"
+ integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0"
+ integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.13.2":
+ version "1.13.2"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1"
+ integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==
+
+"@webassemblyjs/wasm-edit@^1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597"
+ integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@webassemblyjs/helper-buffer" "1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.13.2"
+ "@webassemblyjs/helper-wasm-section" "1.14.1"
+ "@webassemblyjs/wasm-gen" "1.14.1"
+ "@webassemblyjs/wasm-opt" "1.14.1"
+ "@webassemblyjs/wasm-parser" "1.14.1"
+ "@webassemblyjs/wast-printer" "1.14.1"
+
+"@webassemblyjs/wasm-gen@1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570"
+ integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.13.2"
+ "@webassemblyjs/ieee754" "1.13.2"
+ "@webassemblyjs/leb128" "1.13.2"
+ "@webassemblyjs/utf8" "1.13.2"
+
+"@webassemblyjs/wasm-opt@1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b"
+ integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@webassemblyjs/helper-buffer" "1.14.1"
+ "@webassemblyjs/wasm-gen" "1.14.1"
+ "@webassemblyjs/wasm-parser" "1.14.1"
+
+"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb"
+ integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@webassemblyjs/helper-api-error" "1.13.2"
+ "@webassemblyjs/helper-wasm-bytecode" "1.13.2"
+ "@webassemblyjs/ieee754" "1.13.2"
+ "@webassemblyjs/leb128" "1.13.2"
+ "@webassemblyjs/utf8" "1.13.2"
+
+"@webassemblyjs/wast-printer@1.14.1":
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07"
+ integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==
+ dependencies:
+ "@webassemblyjs/ast" "1.14.1"
+ "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-3.0.1.tgz#76ac285b9658fa642ce238c276264589aa2b6b57"
+ integrity sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==
+
+"@webpack-cli/info@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-3.0.1.tgz#3cff37fabb7d4ecaab6a8a4757d3826cf5888c63"
+ integrity sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==
+
+"@webpack-cli/serve@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-3.0.1.tgz#bd8b1f824d57e30faa19eb78e4c0951056f72f00"
+ integrity sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+accepts@~1.3.4, accepts@~1.3.8:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
+acorn-import-phases@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
+ integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
+
+acorn@^8.15.0:
+ version "8.15.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
+ integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
+
+ajv-formats@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
+ integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
+ dependencies:
+ ajv "^8.0.0"
+
+ajv-keywords@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
+ integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+
+ajv@^8.0.0, ajv@^8.9.0:
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
+ integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+ fast-uri "^3.0.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+
+ansi-html-community@^0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
+ integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-regex@^6.0.1:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1"
+ integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+ansi-styles@^6.1.0:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041"
+ integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64id@2.0.0, base64id@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+ integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
+baseline-browser-mapping@^2.8.19:
+ version "2.8.25"
+ resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz#947dc6f81778e0fa0424a2ab9ea09a3033e71109"
+ integrity sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==
+
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+ integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
+
+binary-extensions@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
+ integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+
+body-parser@1.20.3, body-parser@^1.19.0:
+ version "1.20.3"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
+ integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
+ dependencies:
+ bytes "3.1.2"
+ content-type "~1.0.5"
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ on-finished "2.4.1"
+ qs "6.13.0"
+ raw-body "2.5.2"
+ type-is "~1.6.18"
+ unpipe "1.0.0"
+
+bonjour-service@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.3.0.tgz#80d867430b5a0da64e82a8047fc1e355bdb71722"
+ integrity sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+ multicast-dns "^7.2.5"
+
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
+ integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@^3.0.3, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browser-stdout@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+ integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+browserslist@^4.24.0:
+ version "4.27.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.27.0.tgz#755654744feae978fbb123718b2f139bc0fa6697"
+ integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
+ dependencies:
+ baseline-browser-mapping "^2.8.19"
+ caniuse-lite "^1.0.30001751"
+ electron-to-chromium "^1.5.238"
+ node-releases "^2.0.26"
+ update-browserslist-db "^1.1.4"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+bundle-name@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
+ integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==
+ dependencies:
+ run-applescript "^7.0.0"
+
+bytes@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+call-bound@^1.0.2, call-bound@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ get-intrinsic "^1.3.0"
+
+camelcase@^6.0.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+caniuse-lite@^1.0.30001751:
+ version "1.0.30001754"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz#7758299d9a72cce4e6b038788a15b12b44002759"
+ integrity sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==
+
+chalk@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+chokidar@^3.5.1, chokidar@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chokidar@^4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
+ integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
+ dependencies:
+ readdirp "^4.0.1"
+
+chrome-trace-event@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b"
+ integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==
+
+cliui@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+ integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
+cliui@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+ integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^7.0.0"
+
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colorette@^2.0.10, colorette@^2.0.14:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+
+commander@^12.1.0:
+ version "12.1.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+ integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
+commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+compressible@~2.0.18:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+ integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ dependencies:
+ mime-db ">= 1.43.0 < 2"
+
+compression@^1.7.4:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
+ integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
+ dependencies:
+ bytes "3.1.2"
+ compressible "~2.0.18"
+ debug "2.6.9"
+ negotiator "~0.6.4"
+ on-headers "~1.1.0"
+ safe-buffer "5.2.1"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+connect-history-api-fallback@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
+ integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
+
+connect@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+ integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+ dependencies:
+ debug "2.6.9"
+ finalhandler "1.1.2"
+ parseurl "~1.3.3"
+ utils-merge "1.0.1"
+
+content-disposition@0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+ integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+ dependencies:
+ safe-buffer "5.2.1"
+
+content-type@~1.0.4, content-type@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
+ integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
+
+cookie@~0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cors@~2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
+cross-spawn@^7.0.3, cross-spawn@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+custom-event@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+ integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
+
+date-format@^4.0.14:
+ version "4.0.14"
+ resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400"
+ integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^4.1.0, debug@^4.3.4, debug@^4.3.5:
+ version "4.4.3"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
+ integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
+ dependencies:
+ ms "^2.1.3"
+
+debug@~4.3.1, debug@~4.3.2, debug@~4.3.4:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
+ integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+ dependencies:
+ ms "^2.1.3"
+
+decamelize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+ integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+default-browser-id@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26"
+ integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==
+
+default-browser@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf"
+ integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==
+ dependencies:
+ bundle-name "^4.1.0"
+ default-browser-id "^5.0.0"
+
+define-lazy-prop@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
+ integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
+
+destroy@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+ integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+detect-node@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
+ integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+
+di@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+ integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==
+
+diff@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a"
+ integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==
+
+dns-packet@^5.2.2:
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f"
+ integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==
+ dependencies:
+ "@leichtgewicht/ip-codec" "^2.0.1"
+
+dom-serialize@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+ integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==
+ dependencies:
+ custom-event "~1.0.0"
+ ent "~2.2.0"
+ extend "^3.0.0"
+ void-elements "^2.0.0"
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
+electron-to-chromium@^1.5.238:
+ version "1.5.248"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz#e69941494efbc69ebf2803517f4e85d016013d4b"
+ integrity sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
+encodeurl@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+ integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+
+engine.io-parser@~5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
+ integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
+
+engine.io@~6.6.0:
+ version "6.6.4"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee"
+ integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==
+ dependencies:
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
+ accepts "~1.3.4"
+ base64id "2.0.0"
+ cookie "~0.7.2"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.2.1"
+ ws "~8.17.1"
+
+enhanced-resolve@^5.17.3:
+ version "5.18.4"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828"
+ integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+ent@~2.2.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85"
+ integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==
+ dependencies:
+ call-bound "^1.0.3"
+ es-errors "^1.3.0"
+ punycode "^1.4.1"
+ safe-regex-test "^1.1.0"
+
+envinfo@^7.14.0:
+ version "7.20.0"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.20.0.tgz#3fd9de69fb6af3e777a017dfa033676368d67dd7"
+ integrity sha512-+zUomDcLXsVkQ37vUqWBvQwLaLlj8eZPSi61llaEFAVBY5mhcXdaSw1pSJVl4yTYD5g/gEfpNl28YYk4IPvrrg==
+
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-module-lexer@^1.2.1:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a"
+ integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+escalade@^3.1.1, escalade@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-scope@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
+eventemitter3@^4.0.0:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+events@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+express@^4.21.2:
+ version "4.21.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
+ integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
+ dependencies:
+ accepts "~1.3.8"
+ array-flatten "1.1.1"
+ body-parser "1.20.3"
+ content-disposition "0.5.4"
+ content-type "~1.0.4"
+ cookie "0.7.1"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "2.0.0"
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.3.1"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ merge-descriptors "1.0.3"
+ methods "~1.1.2"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.12"
+ proxy-addr "~2.0.7"
+ qs "6.13.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.2.1"
+ send "0.19.0"
+ serve-static "1.16.2"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-uri@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
+ integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
+
+fastest-levenshtein@^1.0.12:
+ version "1.0.16"
+ resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
+ integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
+
+faye-websocket@^0.11.3:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
+ integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+finalhandler@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+finalhandler@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
+ integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ statuses "2.0.1"
+ unpipe "~1.0.0"
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+flat@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+ integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+flatted@^3.2.7:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
+ integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
+
+follow-redirects@^1.0.0:
+ version "1.15.11"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
+ integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
+
+foreground-child@^3.1.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
+ integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
+ dependencies:
+ cross-spawn "^7.0.6"
+ signal-exit "^4.0.1"
+
+format-util@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271"
+ integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==
+
+forwarded@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+ integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
+fs-extra@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-to-regex.js@^1.0.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413"
+ integrity sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==
+
+glob-to-regexp@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+ integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@^10.4.5:
+ version "10.4.5"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
+ integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
+ dependencies:
+ foreground-child "^3.1.0"
+ jackspeak "^3.1.2"
+ minimatch "^9.0.4"
+ minipass "^7.1.2"
+ package-json-from-dist "^1.0.0"
+ path-scurry "^1.11.1"
+
+glob@^7.1.3, glob@^7.1.7:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+handle-thing@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
+ integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+he@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+http-deceiver@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+ integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==
+
+http-errors@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+ integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+ dependencies:
+ depd "2.0.0"
+ inherits "2.0.4"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ toidentifier "1.0.1"
+
+http-errors@~1.6.2:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
+http-parser-js@>=0.5.1:
+ version "0.5.10"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075"
+ integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==
+
+http-proxy-middleware@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef"
+ integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==
+ dependencies:
+ "@types/http-proxy" "^1.17.8"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ is-plain-obj "^3.0.0"
+ micromatch "^4.0.2"
+
+http-proxy@^1.18.1:
+ version "1.18.1"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+ integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+ dependencies:
+ eventemitter3 "^4.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+hyperdyperid@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b"
+ integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+import-local@^3.0.2:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260"
+ integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
+interpret@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
+ integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+ipaddr.js@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
+ integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.16.1:
+ version "2.16.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
+ integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
+ dependencies:
+ hasown "^2.0.2"
+
+is-docker@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200"
+ integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-inside-container@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
+ integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==
+ dependencies:
+ is-docker "^3.0.0"
+
+is-network-error@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.0.tgz#2ce62cbca444abd506f8a900f39d20b898d37512"
+ integrity sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+ integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-plain-obj@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
+ integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
+
+is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-regex@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
+ integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==
+ dependencies:
+ call-bound "^1.0.2"
+ gopd "^1.2.0"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+is-unicode-supported@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+ integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
+is-wsl@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2"
+ integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==
+ dependencies:
+ is-inside-container "^1.0.0"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+isbinaryfile@^4.0.8:
+ version "4.0.10"
+ resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3"
+ integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+jackspeak@^3.1.2:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
+ integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+ optionalDependencies:
+ "@pkgjs/parseargs" "^0.11.0"
+
+jest-worker@^27.4.5:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+ integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+ dependencies:
+ "@types/node" "*"
+ merge-stream "^2.0.0"
+ supports-color "^8.0.0"
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-parse-even-better-errors@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+karma-chrome-launcher@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9"
+ integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==
+ dependencies:
+ which "^1.2.1"
+
+karma-mocha@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d"
+ integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==
+ dependencies:
+ minimist "^1.2.3"
+
+karma-sourcemap-loader@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488"
+ integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA==
+ dependencies:
+ graceful-fs "^4.2.10"
+
+karma-webpack@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.1.tgz#4eafd31bbe684a747a6e8f3e4ad373e53979ced4"
+ integrity sha512-oo38O+P3W2mSPCSUrQdySSPv1LvPpXP+f+bBimNomS5sW+1V4SuhCuW8TfJzV+rDv921w2fDSDw0xJbPe6U+kQ==
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^9.0.3"
+ webpack-merge "^4.1.5"
+
+"karma@github:Kotlin/karma#6.4.5":
+ version "6.4.4"
+ resolved "https://codeload.github.com/Kotlin/karma/tar.gz/239a8fc984584f0d96b1dd750e7a5e2c79da93a6"
+ dependencies:
+ "@colors/colors" "1.5.0"
+ body-parser "^1.19.0"
+ braces "^3.0.2"
+ chokidar "^3.5.1"
+ connect "^3.7.0"
+ di "^0.0.1"
+ dom-serialize "^2.2.1"
+ glob "^7.1.7"
+ graceful-fs "^4.2.6"
+ http-proxy "^1.18.1"
+ isbinaryfile "^4.0.8"
+ lodash "^4.17.21"
+ log4js "^6.4.1"
+ mime "^2.5.2"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.5"
+ qjobs "^1.2.0"
+ range-parser "^1.2.1"
+ rimraf "^3.0.2"
+ socket.io "^4.7.2"
+ source-map "^0.6.1"
+ tmp "^0.2.1"
+ ua-parser-js "^0.7.30"
+ yargs "^16.1.1"
+
+kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+kotlin-web-helpers@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-3.0.0.tgz#3ed6b48f694f74bb60a737a9d7e2c0e3b29abdb9"
+ integrity sha512-kdQO4AJQkUPvpLh9aglkXDRyN+CfXO7pKq+GESEnxooBFkQpytLrqZis3ABvmFN1cGw/ZQ/K38u5sRGW+NfBnw==
+ dependencies:
+ format-util "^1.0.5"
+
+launch-editor@^2.6.1:
+ version "2.12.0"
+ resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.12.0.tgz#cc740f4e0263a6b62ead2485f9896e545321f817"
+ integrity sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==
+ dependencies:
+ picocolors "^1.1.1"
+ shell-quote "^1.8.3"
+
+loader-runner@^4.2.0:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.1.tgz#6c76ed29b0ccce9af379208299f07f876de737e3"
+ integrity sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash@^4.17.15, lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+log-symbols@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+ integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+ dependencies:
+ chalk "^4.1.0"
+ is-unicode-supported "^0.1.0"
+
+log4js@^6.4.1:
+ version "6.9.1"
+ resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6"
+ integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==
+ dependencies:
+ date-format "^4.0.14"
+ debug "^4.3.4"
+ flatted "^3.2.7"
+ rfdc "^1.3.0"
+ streamroller "^3.1.5"
+
+lru-cache@^10.2.0:
+ version "10.4.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
+ integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+memfs@^4.43.1:
+ version "4.50.0"
+ resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.50.0.tgz#1832177d5592ec1e6a816fb4fe01012ada2856e7"
+ integrity sha512-N0LUYQMUA1yS5tJKmMtU9yprPm6ZIg24yr/OVv/7t6q0kKDIho4cBbXRi1XKttUmNYDYgF/q45qrKE/UhGO0CA==
+ dependencies:
+ "@jsonjoy.com/json-pack" "^1.11.0"
+ "@jsonjoy.com/util" "^1.9.0"
+ glob-to-regex.js "^1.0.1"
+ thingies "^2.5.0"
+ tree-dump "^1.0.3"
+ tslib "^2.0.0"
+
+merge-descriptors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
+ integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
+micromatch@^4.0.2:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0:
+ version "1.54.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
+ integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
+
+mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mime-types@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce"
+ integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==
+ dependencies:
+ mime-db "^1.54.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mime@^2.5.2:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+ integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
+minimalistic-assert@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimatch@^3.0.4, minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^9.0.3, minimatch@^9.0.4, minimatch@^9.0.5:
+ version "9.0.5"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
+ integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimist@^1.2.3, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
+ integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+
+mkdirp@^0.5.5:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
+mocha@11.7.2:
+ version "11.7.2"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.2.tgz#3c0079fe5cc2f8ea86d99124debcc42bb1ab22b5"
+ integrity sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==
+ dependencies:
+ browser-stdout "^1.3.1"
+ chokidar "^4.0.1"
+ debug "^4.3.5"
+ diff "^7.0.0"
+ escape-string-regexp "^4.0.0"
+ find-up "^5.0.0"
+ glob "^10.4.5"
+ he "^1.2.0"
+ js-yaml "^4.1.0"
+ log-symbols "^4.1.0"
+ minimatch "^9.0.5"
+ ms "^2.1.3"
+ picocolors "^1.1.1"
+ serialize-javascript "^6.0.2"
+ strip-json-comments "^3.1.1"
+ supports-color "^8.1.1"
+ workerpool "^9.2.0"
+ yargs "^17.7.2"
+ yargs-parser "^21.1.1"
+ yargs-unparser "^2.0.0"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+ms@2.1.3, ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+multicast-dns@^7.2.5:
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
+ integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==
+ dependencies:
+ dns-packet "^5.2.2"
+ thunky "^1.0.2"
+
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+negotiator@~0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
+ integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
+
+neo-async@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-forge@^1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
+ integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
+
+node-releases@^2.0.26:
+ version "2.0.27"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e"
+ integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-assign@^4:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-inspect@^1.13.3:
+ version "1.13.4"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+ integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
+
+obuf@^1.0.0, obuf@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+ integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+
+on-finished@2.4.1, on-finished@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+ integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+ dependencies:
+ ee-first "1.1.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65"
+ integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+open@^10.0.3:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c"
+ integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==
+ dependencies:
+ default-browser "^5.2.1"
+ define-lazy-prop "^3.0.0"
+ is-inside-container "^1.0.0"
+ wsl-utils "^0.1.0"
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-retry@^6.2.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af"
+ integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==
+ dependencies:
+ "@types/retry" "0.12.2"
+ is-network-error "^1.0.0"
+ retry "^0.13.1"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+package-json-from-dist@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
+ integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
+
+parseurl@~1.3.2, parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-scurry@^1.11.1:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
+ integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
+ dependencies:
+ lru-cache "^10.2.0"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+
+path-to-regexp@0.1.12:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
+ integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+proxy-addr@~2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+ integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+ dependencies:
+ forwarded "0.2.0"
+ ipaddr.js "1.9.1"
+
+punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==
+
+qjobs@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+ integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
+
+qs@6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+ integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+ dependencies:
+ side-channel "^1.0.6"
+
+randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+range-parser@^1.2.1, range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
+ dependencies:
+ bytes "3.1.2"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+readable-stream@^2.0.1:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.0.6:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
+ integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^4.0.1:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
+ integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+rechoir@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
+ integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
+ dependencies:
+ resolve "^1.20.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.20.0:
+ version "1.22.11"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
+ integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
+ dependencies:
+ is-core-module "^2.16.1"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+retry@^0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
+ integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+
+rfdc@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
+ integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
+
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+run-applescript@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911"
+ integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==
+
+safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex-test@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1"
+ integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ is-regex "^1.2.1"
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+schema-utils@^4.0.0, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.2:
+ version "4.3.3"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46"
+ integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==
+ dependencies:
+ "@types/json-schema" "^7.0.9"
+ ajv "^8.9.0"
+ ajv-formats "^2.1.1"
+ ajv-keywords "^5.1.0"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+ integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==
+
+selfsigned@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0"
+ integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==
+ dependencies:
+ "@types/node-forge" "^1.3.0"
+ node-forge "^1"
+
+send@0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
+ integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
+ dependencies:
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ mime "1.6.0"
+ ms "2.1.3"
+ on-finished "2.4.1"
+ range-parser "~1.2.1"
+ statuses "2.0.1"
+
+serialize-javascript@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+ integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
+ dependencies:
+ randombytes "^2.1.0"
+
+serve-index@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==
+ dependencies:
+ accepts "~1.3.4"
+ batch "0.6.1"
+ debug "2.6.9"
+ escape-html "~1.0.3"
+ http-errors "~1.6.2"
+ mime-types "~2.1.17"
+ parseurl "~1.3.2"
+
+serve-static@1.16.2:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
+ integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
+ dependencies:
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.19.0"
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+setprototypeof@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+shell-quote@^1.8.3:
+ version "1.8.3"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
+ integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
+
+side-channel-list@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
+ integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+
+side-channel-map@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+ integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+
+side-channel-weakmap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+ integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+ side-channel-map "^1.0.1"
+
+side-channel@^1.0.6:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
+ integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+ side-channel-list "^1.0.0"
+ side-channel-map "^1.0.1"
+ side-channel-weakmap "^1.0.2"
+
+signal-exit@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
+socket.io-adapter@~2.5.2:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082"
+ integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==
+ dependencies:
+ debug "~4.3.4"
+ ws "~8.17.1"
+
+socket.io-parser@~4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+ integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@^4.7.2:
+ version "4.8.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a"
+ integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ cors "~2.8.5"
+ debug "~4.3.2"
+ engine.io "~6.6.0"
+ socket.io-adapter "~2.5.2"
+ socket.io-parser "~4.2.4"
+
+sockjs@^0.3.24:
+ version "0.3.24"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
+ integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==
+ dependencies:
+ faye-websocket "^0.11.3"
+ uuid "^8.3.2"
+ websocket-driver "^0.7.4"
+
+source-map-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map-loader@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-5.0.0.tgz#f593a916e1cc54471cfc8851b905c8a845fc7e38"
+ integrity sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==
+ dependencies:
+ iconv-lite "^0.6.3"
+ source-map-js "^1.0.2"
+
+source-map-support@~0.5.20:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdy-transport@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+ integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+ dependencies:
+ debug "^4.1.0"
+ detect-node "^2.0.4"
+ hpack.js "^2.1.6"
+ obuf "^1.1.2"
+ readable-stream "^3.0.6"
+ wbuf "^1.7.3"
+
+spdy@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b"
+ integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
+ dependencies:
+ debug "^4.1.0"
+ handle-thing "^2.0.0"
+ http-deceiver "^1.2.7"
+ select-hose "^2.0.0"
+ spdy-transport "^3.0.0"
+
+statuses@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+ integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+"statuses@>= 1.4.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
+
+streamroller@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff"
+ integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==
+ dependencies:
+ date-format "^4.0.14"
+ debug "^4.3.4"
+ fs-extra "^8.1.0"
+
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^7.0.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
+ integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==
+ dependencies:
+ ansi-regex "^6.0.1"
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^8.0.0, supports-color@^8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.1.1, tapable@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
+ integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
+
+terser-webpack-plugin@^5.3.11:
+ version "5.3.14"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06"
+ integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.25"
+ jest-worker "^27.4.5"
+ schema-utils "^4.3.0"
+ serialize-javascript "^6.0.2"
+ terser "^5.31.1"
+
+terser@^5.31.1:
+ version "5.44.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c"
+ integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==
+ dependencies:
+ "@jridgewell/source-map" "^0.3.3"
+ acorn "^8.15.0"
+ commander "^2.20.0"
+ source-map-support "~0.5.20"
+
+thingies@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f"
+ integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==
+
+thunky@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
+ integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+
+tmp@^0.2.1:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8"
+ integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+tree-dump@^1.0.3, tree-dump@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4"
+ integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==
+
+tslib@^2.0.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+ua-parser-js@^0.7.30:
+ version "0.7.41"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452"
+ integrity sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==
+
+undici-types@~7.16.0:
+ version "7.16.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
+ integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
+
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
+update-browserslist-db@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a"
+ integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+uuid@^8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+vary@^1, vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+void-elements@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+ integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==
+
+watchpack@^2.4.1:
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947"
+ integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
+wbuf@^1.1.0, wbuf@^1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+ integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+webpack-cli@6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-6.0.1.tgz#a1ce25da5ba077151afd73adfa12e208e5089207"
+ integrity sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==
+ dependencies:
+ "@discoveryjs/json-ext" "^0.6.1"
+ "@webpack-cli/configtest" "^3.0.1"
+ "@webpack-cli/info" "^3.0.1"
+ "@webpack-cli/serve" "^3.0.1"
+ colorette "^2.0.14"
+ commander "^12.1.0"
+ cross-spawn "^7.0.3"
+ envinfo "^7.14.0"
+ fastest-levenshtein "^1.0.12"
+ import-local "^3.0.2"
+ interpret "^3.1.1"
+ rechoir "^0.8.0"
+ webpack-merge "^6.0.1"
+
+webpack-dev-middleware@^7.4.2:
+ version "7.4.5"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz#d4e8720aa29cb03bc158084a94edb4594e3b7ac0"
+ integrity sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==
+ dependencies:
+ colorette "^2.0.10"
+ memfs "^4.43.1"
+ mime-types "^3.0.1"
+ on-finished "^2.4.1"
+ range-parser "^1.2.1"
+ schema-utils "^4.0.0"
+
+webpack-dev-server@5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz#96a143d50c58fef0c79107e61df911728d7ceb39"
+ integrity sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==
+ dependencies:
+ "@types/bonjour" "^3.5.13"
+ "@types/connect-history-api-fallback" "^1.5.4"
+ "@types/express" "^4.17.21"
+ "@types/express-serve-static-core" "^4.17.21"
+ "@types/serve-index" "^1.9.4"
+ "@types/serve-static" "^1.15.5"
+ "@types/sockjs" "^0.3.36"
+ "@types/ws" "^8.5.10"
+ ansi-html-community "^0.0.8"
+ bonjour-service "^1.2.1"
+ chokidar "^3.6.0"
+ colorette "^2.0.10"
+ compression "^1.7.4"
+ connect-history-api-fallback "^2.0.0"
+ express "^4.21.2"
+ graceful-fs "^4.2.6"
+ http-proxy-middleware "^2.0.9"
+ ipaddr.js "^2.1.0"
+ launch-editor "^2.6.1"
+ open "^10.0.3"
+ p-retry "^6.2.0"
+ schema-utils "^4.2.0"
+ selfsigned "^2.4.1"
+ serve-index "^1.9.1"
+ sockjs "^0.3.24"
+ spdy "^4.0.2"
+ webpack-dev-middleware "^7.4.2"
+ ws "^8.18.0"
+
+webpack-merge@^4.1.5:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d"
+ integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==
+ dependencies:
+ lodash "^4.17.15"
+
+webpack-merge@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a"
+ integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==
+ dependencies:
+ clone-deep "^4.0.1"
+ flat "^5.0.2"
+ wildcard "^2.0.1"
+
+webpack-sources@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.3.tgz#d4bf7f9909675d7a070ff14d0ef2a4f3c982c723"
+ integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
+
+webpack@5.101.3:
+ version "5.101.3"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346"
+ integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==
+ dependencies:
+ "@types/eslint-scope" "^3.7.7"
+ "@types/estree" "^1.0.8"
+ "@types/json-schema" "^7.0.15"
+ "@webassemblyjs/ast" "^1.14.1"
+ "@webassemblyjs/wasm-edit" "^1.14.1"
+ "@webassemblyjs/wasm-parser" "^1.14.1"
+ acorn "^8.15.0"
+ acorn-import-phases "^1.0.3"
+ browserslist "^4.24.0"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.17.3"
+ es-module-lexer "^1.2.1"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.11"
+ json-parse-even-better-errors "^2.3.1"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^4.3.2"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.3.11"
+ watchpack "^2.4.1"
+ webpack-sources "^3.3.3"
+
+websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
+ integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
+ dependencies:
+ http-parser-js ">=0.5.1"
+ safe-buffer ">=5.1.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+ integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
+
+which@^1.2.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wildcard@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
+ integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
+
+workerpool@^9.2.0:
+ version "9.3.4"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41"
+ integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+ dependencies:
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@8.18.3, ws@^8.18.0:
+ version "8.18.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
+ integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
+
+ws@~8.17.1:
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
+
+wsl-utils@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.1.0.tgz#8783d4df671d4d50365be2ee4c71917a0557baab"
+ integrity sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==
+ dependencies:
+ is-wsl "^3.1.0"
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yargs-parser@^20.2.2:
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs-parser@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs-unparser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+ integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+ dependencies:
+ camelcase "^6.0.0"
+ decamelize "^4.0.0"
+ flat "^5.0.2"
+ is-plain-obj "^2.1.0"
+
+yargs@^16.1.1:
+ version "16.2.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+ integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.5"
+ yargs-parser "^20.2.2"
+
+yargs@^17.7.2:
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+ dependencies:
+ cliui "^8.0.1"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.3"
+ y18n "^5.0.5"
+ yargs-parser "^21.1.1"
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==