Merge branch 'dev' into 'main'
Merge Sprint 4 Closes #42 See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!47
This commit is contained in:
582
Cargo.lock
generated
582
Cargo.lock
generated
@@ -210,7 +210,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"itertools 0.13.0",
|
||||
"nom",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
@@ -1638,6 +1638,15 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.5.1"
|
||||
@@ -1892,7 +1901,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1984,6 +1993,15 @@ dependencies = [
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -2029,6 +2047,16 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.0"
|
||||
@@ -2079,6 +2107,16 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
@@ -2106,6 +2144,17 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "disqualified"
|
||||
version = "1.0.0"
|
||||
@@ -2186,6 +2235,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -2330,6 +2388,15 @@ dependencies = [
|
||||
"ttf-parser 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -2337,7 +2404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2351,12 +2418,27 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -2391,6 +2473,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gethostname"
|
||||
version = "1.1.0"
|
||||
@@ -2693,6 +2785,124 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.8"
|
||||
@@ -2927,6 +3137,12 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "0.4.2"
|
||||
@@ -2999,7 +3215,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"block",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"log",
|
||||
"objc",
|
||||
"paste",
|
||||
@@ -3075,6 +3291,23 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
@@ -3496,6 +3729,50 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.74"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -3663,6 +3940,10 @@ dependencies = [
|
||||
"directories",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
"tungstenite",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -3681,6 +3962,15 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pp-rs"
|
||||
version = "0.2.1"
|
||||
@@ -3913,6 +4203,7 @@ checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"lewton",
|
||||
"symphonia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4019,6 +4310,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scoped-tls"
|
||||
version = "1.0.1"
|
||||
@@ -4044,6 +4344,29 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework-sys"
|
||||
version = "2.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.2.0"
|
||||
@@ -4099,6 +4422,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -4233,9 +4567,15 @@ version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
@@ -4249,6 +4589,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svg_fmt"
|
||||
version = "0.4.5"
|
||||
@@ -4266,6 +4618,55 @@ dependencies = [
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-bundle-mp3",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-bundle-mp3"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.106"
|
||||
@@ -4277,6 +4678,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
@@ -4312,6 +4724,19 @@ dependencies = [
|
||||
"slotmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
@@ -4395,6 +4820,16 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
@@ -4542,6 +4977,24 @@ version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror 2.0.17",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.2"
|
||||
@@ -4554,6 +5007,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "typewit"
|
||||
version = "1.14.2"
|
||||
@@ -4620,6 +5079,30 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
@@ -4649,6 +5132,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
@@ -5581,6 +6070,12 @@ version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
version = "2.21.0"
|
||||
@@ -5650,6 +6145,29 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.3"
|
||||
@@ -5675,3 +6193,57 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -14,12 +14,16 @@ codegen-units = 1
|
||||
lto = "thin"
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.17.2"
|
||||
bevy = { version = "0.17.2", features = ["mp3"] }
|
||||
bevy_aseprite_ultra = "0.7.0"
|
||||
bevy_dev_tools = "0.17.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
directories = "6.0"
|
||||
tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
||||
url = "2.5.7"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.27.2"
|
||||
|
||||
[dev-dependencies]
|
||||
uuid = "1.18.1"
|
||||
|
||||
@@ -30,6 +30,7 @@ cargo run
|
||||
|
||||
1. [Install Rust](https://rust-lang.org/tools/install/)
|
||||
2. [Install Bevy OS depedencies](https://bevy.org/learn/quick-start/getting-started/setup/#installing-os-dependencies)
|
||||
3. Install `openssl`
|
||||
|
||||
```sh
|
||||
git clone https://gitlab.uni-ulm.de/softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik pomomon-garden
|
||||
|
||||
BIN
assets/achievement.aseprite
Normal file
BIN
assets/achievement.aseprite
Normal file
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"grid_width": 12,
|
||||
"grid_height": 4,
|
||||
"grid_width": 15,
|
||||
"grid_height": 5,
|
||||
"pom_speed": 1.5,
|
||||
"shovel_base_price": 10,
|
||||
"shovel_rate": 0.5,
|
||||
"shovel_rate": 0.2,
|
||||
"berry_seeds": [
|
||||
{
|
||||
"name": "Normale Samen",
|
||||
@@ -26,5 +26,7 @@
|
||||
"slice": "Seed3",
|
||||
"growth_stages": 6
|
||||
}
|
||||
]
|
||||
],
|
||||
"wonder_event_url": "wss://pomomon.farm/ws",
|
||||
"berries_per_focus_minute": 1
|
||||
}
|
||||
BIN
assets/shovel.aseprite
Normal file
BIN
assets/shovel.aseprite
Normal file
Binary file not shown.
BIN
assets/sounds/beep.mp3
Normal file
BIN
assets/sounds/beep.mp3
Normal file
Binary file not shown.
@@ -48,6 +48,8 @@
|
||||
libxkbcommon
|
||||
# linker
|
||||
lld
|
||||
|
||||
openssl
|
||||
];
|
||||
runtimeLibs = pkgs.lib.makeLibraryPath bevyDeps;
|
||||
|
||||
|
||||
85
src/features/achievement/components.rs
Normal file
85
src/features/achievement/components.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::{features::phase::components::SessionTracker, prelude::*};
|
||||
use std::collections::HashMap;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
/// Represents an unlockable achievement.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, EnumIter)]
|
||||
pub enum AchievementId {
|
||||
// Berry achievements
|
||||
FirstSteps,
|
||||
MasterFarmer,
|
||||
BerryTycoon,
|
||||
// Focus achievements,
|
||||
GettingStarted,
|
||||
FocusMaster,
|
||||
ZenMaster,
|
||||
// Withered achievements
|
||||
Negligent,
|
||||
CompostKing,
|
||||
}
|
||||
|
||||
impl AchievementId {
|
||||
/// Title to be displayed ingame
|
||||
pub fn title(&self) -> String {
|
||||
match self {
|
||||
AchievementId::FirstSteps => "Erste Schritte",
|
||||
AchievementId::MasterFarmer => "Meisterbauer",
|
||||
AchievementId::BerryTycoon => "Beeren-Tycoon",
|
||||
AchievementId::GettingStarted => "Aller Anfang",
|
||||
AchievementId::FocusMaster => "Fokus-Meister",
|
||||
AchievementId::ZenMaster => "Zen-Meister",
|
||||
AchievementId::Negligent => "Nachlässig",
|
||||
AchievementId::CompostKing => "Kompost-König",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Description to be displayed ingame
|
||||
pub fn description(&self) -> String {
|
||||
match self {
|
||||
AchievementId::FirstSteps => "Verdiene eine Beere.",
|
||||
AchievementId::MasterFarmer => "Verdiene 100 Beeren.",
|
||||
AchievementId::BerryTycoon => "Verdiene 1.000 Beeren.",
|
||||
AchievementId::GettingStarted => "Schließe deine erste Fokus-Phase ab.",
|
||||
AchievementId::FocusMaster => "Schließe 10 Fokus-Phasen ab.",
|
||||
AchievementId::ZenMaster => "Schließe 50 Fokus-Phasen ab.",
|
||||
AchievementId::Negligent => "Lasse eine Pflanze verdorren.",
|
||||
AchievementId::CompostKing => "Lasse 10 Pflanzen verdorren.",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Label to be displayed ingame (Title: Description)
|
||||
pub fn label(&self) -> String {
|
||||
format!("{}: {}", self.title(), self.description())
|
||||
}
|
||||
|
||||
/// Checks if an achievement's conditions are met
|
||||
pub fn conditions_met(&self, tracker: &SessionTracker) -> bool {
|
||||
match self {
|
||||
AchievementId::FirstSteps => tracker.total_berries_earned >= 1,
|
||||
AchievementId::MasterFarmer => tracker.total_berries_earned >= 100,
|
||||
AchievementId::BerryTycoon => tracker.total_berries_earned >= 1000,
|
||||
AchievementId::GettingStarted => tracker.completed_focus_phases >= 1,
|
||||
AchievementId::FocusMaster => tracker.completed_focus_phases >= 10,
|
||||
AchievementId::ZenMaster => tracker.completed_focus_phases >= 50,
|
||||
AchievementId::Negligent => tracker.total_plants_withered >= 1,
|
||||
AchievementId::CompostKing => tracker.total_plants_withered >= 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AchievementProgress {
|
||||
pub unlocked: HashMap<AchievementId, bool>,
|
||||
}
|
||||
|
||||
impl AchievementProgress {
|
||||
pub fn is_unlocked(&self, id: &AchievementId) -> bool {
|
||||
*self.unlocked.get(id).unwrap_or(&false)
|
||||
}
|
||||
|
||||
pub fn unlock(&mut self, id: AchievementId) {
|
||||
self.unlocked.insert(id, true);
|
||||
}
|
||||
}
|
||||
33
src/features/achievement/mod.rs
Normal file
33
src/features/achievement/mod.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use crate::features::notification::components::Notifications;
|
||||
use crate::features::phase::components::SessionTracker;
|
||||
use crate::prelude::*;
|
||||
use components::{AchievementId, AchievementProgress};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
pub struct AchievementPlugin;
|
||||
|
||||
impl Plugin for AchievementPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<AchievementProgress>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
check_achievements.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_achievements(
|
||||
tracker: Res<SessionTracker>,
|
||||
mut progress: ResMut<AchievementProgress>,
|
||||
mut notifications: ResMut<Notifications>,
|
||||
) {
|
||||
for achievement in AchievementId::iter() {
|
||||
if !progress.is_unlocked(&achievement) && achievement.conditions_met(&tracker) {
|
||||
progress.unlock(achievement.clone());
|
||||
notifications.info(Some("Erfolg freigeschaltet!"), achievement.label());
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/features/achievement/ui.rs
Normal file
90
src/features/achievement/ui.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::features::achievement::components::{AchievementId, AchievementProgress};
|
||||
use crate::features::ui::ui::popups::spawn_popup;
|
||||
use crate::prelude::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[derive(Component)]
|
||||
pub enum AchievementRootMarker {
|
||||
Menu,
|
||||
}
|
||||
|
||||
pub fn open_achievements_menu(commands: &mut Commands, progress: &AchievementProgress) {
|
||||
spawn_popup(
|
||||
commands,
|
||||
AchievementRootMarker::Menu,
|
||||
"Erfolge",
|
||||
Node {
|
||||
width: px(700),
|
||||
height: px(500),
|
||||
..default()
|
||||
},
|
||||
|parent| {
|
||||
// Scrollable Content
|
||||
parent
|
||||
.spawn((Node {
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
flex_direction: FlexDirection::Column,
|
||||
overflow: Overflow::scroll_y(),
|
||||
padding: UiRect::all(px(10)),
|
||||
row_gap: px(10),
|
||||
..default()
|
||||
},))
|
||||
.with_children(|list| {
|
||||
for id in AchievementId::iter() {
|
||||
let unlocked = progress.is_unlocked(&id);
|
||||
let color = if unlocked {
|
||||
Color::WHITE
|
||||
} else {
|
||||
Color::srgb(0.5, 0.5, 0.5)
|
||||
};
|
||||
|
||||
let bg_color = if unlocked {
|
||||
Color::srgb(0.2, 0.3, 0.2) // Dark Greenish for unlocked
|
||||
} else {
|
||||
Color::srgb(0.1, 0.1, 0.1) // Dark Grey for locked
|
||||
};
|
||||
|
||||
list.spawn((
|
||||
Node {
|
||||
width: percent(100),
|
||||
padding: UiRect::all(px(10)),
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: px(5),
|
||||
border: UiRect::all(px(2)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(bg_color),
|
||||
BorderColor::all(if unlocked {
|
||||
Color::srgb(0.4, 0.8, 0.4)
|
||||
} else {
|
||||
Color::BLACK
|
||||
}),
|
||||
BorderRadius::all(px(5)),
|
||||
))
|
||||
.with_children(|item| {
|
||||
// Title
|
||||
item.spawn(text(id.title(), 20.0, color));
|
||||
// Description
|
||||
item.spawn(text(id.description(), 16.0, color));
|
||||
// Status Text
|
||||
let status = if unlocked {
|
||||
"Freigeschaltet"
|
||||
} else {
|
||||
"Gesperrt"
|
||||
};
|
||||
item.spawn(text(
|
||||
status,
|
||||
12.0,
|
||||
if unlocked {
|
||||
Color::srgb(0.6, 1.0, 0.6)
|
||||
} else {
|
||||
Color::srgb(0.7, 0.3, 0.3)
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use crate::prelude::*;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
/// Global configuration loaded from file, containing balancing numbers and paths.
|
||||
#[derive(Resource, Deserialize, Debug)]
|
||||
pub struct GameConfig {
|
||||
pub grid_width: u32,
|
||||
@@ -10,8 +11,11 @@ pub struct GameConfig {
|
||||
pub shovel_base_price: u32,
|
||||
pub shovel_rate: f32,
|
||||
pub berry_seeds: Vec<BerrySeedConfig>,
|
||||
pub wonder_event_url: String,
|
||||
pub berries_per_focus_minute: u32,
|
||||
}
|
||||
|
||||
/// Configuration for a specific type of seed.
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct BerrySeedConfig {
|
||||
pub name: String,
|
||||
@@ -52,15 +56,19 @@ impl Default for GameConfig {
|
||||
growth_stages: 6,
|
||||
},
|
||||
],
|
||||
wonder_event_url: "wss://pomomon.farm/ws".into(),
|
||||
berries_per_focus_minute: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameConfig {
|
||||
/// Reads `config.json` from assets.
|
||||
pub fn read_config() -> Option<Self> {
|
||||
Self::read_from_path(std::path::Path::new("assets/config.json"))
|
||||
}
|
||||
|
||||
/// Reads configuration from a specific path.
|
||||
pub fn read_from_path(path: &std::path::Path) -> Option<Self> {
|
||||
let file = File::open(path).ok()?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::prelude::*;
|
||||
|
||||
pub mod states;
|
||||
|
||||
/// Handles core engine setup like camera and initial state.
|
||||
pub struct CorePlugin;
|
||||
|
||||
impl Plugin for CorePlugin {
|
||||
@@ -11,6 +12,7 @@ impl Plugin for CorePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the main 2D camera.
|
||||
fn setup_camera(mut commands: Commands) {
|
||||
commands.spawn(Camera2d::default());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Global states of the application.
|
||||
#[derive(States, Clone, PartialEq, Eq, Debug, Hash, Default)]
|
||||
pub enum AppState {
|
||||
#[default]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Plugin for the main game screen, managing the game loop and environment.
|
||||
pub struct GameScreenPlugin;
|
||||
|
||||
impl Plugin for GameScreenPlugin {
|
||||
@@ -9,10 +10,12 @@ impl Plugin for GameScreenPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the game screen environment (e.g., background color).
|
||||
fn setup(mut clear_color: ResMut<ClearColor>) {
|
||||
*clear_color = ClearColor(Color::srgb(0.294, 0.412, 0.184));
|
||||
}
|
||||
|
||||
/// Cleans up resources when exiting the game screen.
|
||||
fn cleanup(mut clear_color: ResMut<ClearColor>) {
|
||||
*clear_color = ClearColor(Color::srgb(0.2, 0.2, 0.2));
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
use super::errors::GridError;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Component representing a single tile on the grid.
|
||||
#[derive(Component)]
|
||||
pub struct Tile {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
/// Visual marker component for the crop on a tile.
|
||||
#[derive(Component)]
|
||||
pub struct CropVisual;
|
||||
|
||||
/// Visual marker component for the water on a tile.
|
||||
#[derive(Component)]
|
||||
pub struct WaterVisual;
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, Clone, Debug)]
|
||||
/// The logical state of a tile.
|
||||
#[derive(Component, Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TileState {
|
||||
#[default]
|
||||
Unclaimed,
|
||||
@@ -38,6 +42,7 @@ impl TileState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource containing grid dimensions and tile entities.
|
||||
#[derive(Resource)]
|
||||
pub struct Grid {
|
||||
pub width: u32,
|
||||
@@ -46,6 +51,7 @@ pub struct Grid {
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
/// Returns the entity of the tile at the given position.
|
||||
pub fn get_tile(&self, pos: (u32, u32)) -> Result<Entity, GridError> {
|
||||
if pos.0 >= self.width || pos.1 >= self.height {
|
||||
return Err(GridError::OutOfBounds {
|
||||
@@ -56,6 +62,7 @@ impl Grid {
|
||||
Ok(self.tiles[pos.0 as usize][pos.1 as usize])
|
||||
}
|
||||
|
||||
/// Modifies the state of a tile using a mapping function.
|
||||
pub fn map_tile_state<F>(
|
||||
&self,
|
||||
pos: (u32, u32),
|
||||
@@ -74,4 +81,19 @@ impl Grid {
|
||||
*tile_state = mapper(&*tile_state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Counts the number of tiles that are not unclaimed.
|
||||
pub fn count_claimed_tiles(&self, tile_query: &Query<&TileState>) -> u32 {
|
||||
self.tiles
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|&entity| {
|
||||
if let Ok(state) = tile_query.get(*entity) {
|
||||
!matches!(state, TileState::Unclaimed)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.count() as u32
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
/// The pixel size of a tile (width and height).
|
||||
pub const TILE_SIZE: f32 = 32.0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
/// Errors related to grid operations.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GridError {
|
||||
OutOfBounds { x: i32, y: i32 },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::prelude::*;
|
||||
use components::{CropVisual, WaterVisual};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub mod components;
|
||||
pub mod consts;
|
||||
pub mod errors;
|
||||
pub mod utils;
|
||||
|
||||
/// Manages the game grid, including tiles, visuals, and updates.
|
||||
pub struct GridPlugin;
|
||||
|
||||
impl Plugin for GridPlugin {
|
||||
@@ -17,6 +19,7 @@ impl Plugin for GridPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the grid and spawns tile entities.
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
||||
let grid_width = config.grid_width;
|
||||
let grid_height = config.grid_height;
|
||||
@@ -26,13 +29,28 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<Gam
|
||||
let mut column = Vec::with_capacity(grid_height as usize);
|
||||
|
||||
for y in 0..grid_height {
|
||||
let initial_state = if x == 1 && y == 1 {
|
||||
TileState::Empty
|
||||
} else {
|
||||
TileState::Unclaimed
|
||||
};
|
||||
|
||||
let tile_entity = commands
|
||||
.spawn((
|
||||
Tile { x, y },
|
||||
TileState::Unclaimed,
|
||||
initial_state.clone(),
|
||||
AseSlice {
|
||||
name: "Unclaimed".into(),
|
||||
aseprite: asset_server.load("tiles/tile-unclaimed.aseprite"),
|
||||
name: match initial_state {
|
||||
TileState::Unclaimed => "Unclaimed",
|
||||
TileState::Empty => "Empty",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.into(),
|
||||
aseprite: asset_server.load(match initial_state {
|
||||
TileState::Unclaimed => "tiles/tile-unclaimed.aseprite",
|
||||
TileState::Empty => "tiles/tile-empty.aseprite",
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
},
|
||||
Sprite::default(),
|
||||
Transform::from_translation(grid_to_world_coords(
|
||||
@@ -80,6 +98,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<Gam
|
||||
});
|
||||
}
|
||||
|
||||
/// Despawns all grid entities and removes resources.
|
||||
fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
||||
for tile_entity in tile_query.iter() {
|
||||
commands.entity(tile_entity).despawn();
|
||||
@@ -87,8 +106,12 @@ fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
||||
commands.remove_resource::<Grid>();
|
||||
}
|
||||
|
||||
/// Updates tile visuals based on their state (e.g., crop growth, highlighting).
|
||||
fn update_tiles(
|
||||
mut query: Query<(&TileState, &mut AseSlice, &Children), (With<Tile>, Without<CropVisual>)>,
|
||||
mut query: Query<
|
||||
(&TileState, &mut AseSlice, &Children, &Tile),
|
||||
(With<Tile>, Without<CropVisual>),
|
||||
>,
|
||||
mut crop_query: Query<
|
||||
(&mut Visibility, &mut Transform, &mut AseSlice),
|
||||
(With<CropVisual>, Without<WaterVisual>, Without<Tile>),
|
||||
@@ -99,8 +122,27 @@ fn update_tiles(
|
||||
>,
|
||||
asset_server: Res<AssetServer>,
|
||||
game_config: Res<GameConfig>,
|
||||
inventory: Res<Inventory>,
|
||||
item_stacks: Query<&ItemStack>,
|
||||
grid: Res<Grid>,
|
||||
mut sprite_query: Query<&mut Sprite, With<Tile>>,
|
||||
) {
|
||||
for (state, mut slice, children) in &mut query {
|
||||
let has_shovel = inventory.has_item_type(&item_stacks, ItemType::Shovel);
|
||||
|
||||
let owned_tiles: HashSet<(u32, u32)> = query
|
||||
.iter()
|
||||
.filter_map(|(state, _, _, tile)| {
|
||||
if !matches!(state, TileState::Unclaimed) {
|
||||
Some((tile.x, tile.y))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (state, mut slice, children, tile) in &mut query {
|
||||
let entity = grid.get_tile((tile.x, tile.y)).unwrap(); // Get entity for sprite query
|
||||
|
||||
slice.name = match state {
|
||||
TileState::Unclaimed => "Unclaimed",
|
||||
TileState::Empty => "Empty",
|
||||
@@ -133,6 +175,34 @@ fn update_tiles(
|
||||
_ => Vec3::ONE,
|
||||
};
|
||||
|
||||
let mut is_highlighted = false;
|
||||
if has_shovel && matches!(state, TileState::Unclaimed) {
|
||||
// Check if not on edge
|
||||
if tile.x > 0 && tile.x < grid.width - 1 && tile.y > 0 && tile.y < grid.height - 1 {
|
||||
// Check neighbors
|
||||
let neighbors = [
|
||||
(tile.x + 1, tile.y),
|
||||
(tile.x.saturating_sub(1), tile.y),
|
||||
(tile.x, tile.y + 1),
|
||||
(tile.x, tile.y.saturating_sub(1)),
|
||||
];
|
||||
for n in neighbors.iter() {
|
||||
if owned_tiles.contains(n) {
|
||||
is_highlighted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut sprite) = sprite_query.get_mut(entity) {
|
||||
if is_highlighted {
|
||||
sprite.color = Color::srgb(0.3, 1.0, 0.3); // Green tint
|
||||
} else {
|
||||
sprite.color = Color::WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
for child in children.iter() {
|
||||
if let Ok((mut visibility, mut transform, mut sprite)) = crop_query.get_mut(child) {
|
||||
*visibility = match state {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use super::errors::GridError;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Calculates the starting X coordinate for centering the grid.
|
||||
pub fn grid_start_x(grid_width: u32) -> f32 {
|
||||
-(grid_width as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
||||
}
|
||||
|
||||
/// Calculates the starting Y coordinate for centering the grid.
|
||||
pub fn grid_start_y(grid_height: u32) -> f32 {
|
||||
-(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
||||
}
|
||||
|
||||
/// Converts world coordinates to grid coordinates.
|
||||
pub fn world_to_grid_coords(
|
||||
world_pos: Vec3,
|
||||
grid_width: u32,
|
||||
@@ -30,6 +33,7 @@ pub fn world_to_grid_coords(
|
||||
Ok((x as u32, y as u32))
|
||||
}
|
||||
|
||||
/// Converts grid coordinates to world coordinates.
|
||||
pub fn grid_to_world_coords(
|
||||
grid_x: u32,
|
||||
grid_y: u32,
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
use crate::{features::phase::components::TimerSettings, prelude::*};
|
||||
|
||||
/// Markers for root UI nodes.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
Status,
|
||||
Settings,
|
||||
ShovelOverlay,
|
||||
}
|
||||
|
||||
/// Markers for text components in the HUD.
|
||||
#[derive(Component)]
|
||||
pub enum TextType {
|
||||
Phase,
|
||||
Timer,
|
||||
}
|
||||
|
||||
/// Markers for buttons in the HUD and settings.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
SettingsOpen,
|
||||
SettingsClose,
|
||||
SettingsExit,
|
||||
SettingsSave,
|
||||
SettingsAchievements,
|
||||
SettingsTimerChange {
|
||||
input: SettingsTimerInput,
|
||||
amount: i32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Types of timers available in the game.
|
||||
#[derive(Clone)]
|
||||
pub enum TimerType {
|
||||
Focus,
|
||||
@@ -32,6 +37,7 @@ pub enum TimerType {
|
||||
}
|
||||
|
||||
impl TimerSettings {
|
||||
/// Changes the duration of a specific timer.
|
||||
pub fn change(&mut self, timer_type: &TimerType, amount: i32) {
|
||||
match timer_type {
|
||||
TimerType::Focus => {
|
||||
@@ -59,6 +65,7 @@ impl TimerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Input types for adjusting timer settings.
|
||||
#[derive(Component, Clone)]
|
||||
pub enum SettingsTimerInput {
|
||||
Minutes(TimerType),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::features::achievement::{components::AchievementProgress, ui::open_achievements_menu};
|
||||
use crate::features::phase::components::TimerSettings;
|
||||
use crate::features::savegame::messages::SavegameDumpMessage;
|
||||
use crate::features::{inventory, shop};
|
||||
@@ -8,6 +9,7 @@ use ui::*;
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin for the Head-Up Display (HUD) containing status bars and buttons.
|
||||
pub struct HudPlugin;
|
||||
|
||||
impl Plugin for HudPlugin {
|
||||
@@ -16,12 +18,19 @@ impl Plugin for HudPlugin {
|
||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(update_status, buttons, update_timer_settings).run_if(in_state(AppState::GameScreen)),
|
||||
(
|
||||
update_status,
|
||||
buttons,
|
||||
update_timer_settings,
|
||||
update_shovel_overlay_visibility,
|
||||
)
|
||||
.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
/// Initializes the HUD UI.
|
||||
fn setup(mut commands: Commands, game_config: Res<GameConfig>, asset_server: Res<AssetServer>) {
|
||||
commands.spawn((
|
||||
RootMarker::Status,
|
||||
Node {
|
||||
@@ -44,34 +53,83 @@ fn setup(mut commands: Commands) {
|
||||
button(
|
||||
shop::components::ButtonType::ShopOpen,
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
padding: UiRect::all(px(10)),
|
||||
..default()
|
||||
},
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Shop [P]", 16.0, color)
|
||||
),
|
||||
button(
|
||||
inventory::components::ButtonType::InventoryOpen,
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
padding: UiRect::all(px(10)),
|
||||
..default()
|
||||
},
|
||||
|color| text("Inventar", 16.0, color)
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Inventar [I]", 16.0, color)
|
||||
),
|
||||
button(
|
||||
ButtonType::SettingsOpen,
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
padding: UiRect::all(px(10)),
|
||||
..default()
|
||||
},
|
||||
|color| text("Einstellungen", 16.0, color)
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Einstellungen [Esc]", 16.0, color)
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
// Shovel Overlay
|
||||
commands.spawn((
|
||||
RootMarker::ShovelOverlay,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: px(20),
|
||||
left: px(0),
|
||||
right: px(0),
|
||||
width: percent(100),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: px(5),
|
||||
..default()
|
||||
},
|
||||
Visibility::Hidden,
|
||||
children![(
|
||||
Node {
|
||||
flex_direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Center,
|
||||
padding: UiRect::all(px(10)),
|
||||
row_gap: px(5),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.7)),
|
||||
BorderRadius::all(px(10)),
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: AlignItems::Center,
|
||||
column_gap: px(10),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
width: px(32),
|
||||
height: px(32),
|
||||
..default()
|
||||
},
|
||||
inventory::components::ItemType::Shovel
|
||||
.get_sprite(&asset_server, &game_config),
|
||||
ImageNode::default()
|
||||
),
|
||||
text("Schaufel-Modus", 20.0, Color::WHITE)
|
||||
]
|
||||
),
|
||||
text(
|
||||
"Klicke auf ein freies Feld, um es freizuschalten.",
|
||||
14.0,
|
||||
Color::WHITE
|
||||
)
|
||||
]
|
||||
)],
|
||||
));
|
||||
}
|
||||
|
||||
/// Updates the status text (phase and timer).
|
||||
fn update_status(phase_res: Res<CurrentPhase>, mut text_query: Query<(&mut Text, &TextType)>) {
|
||||
if !phase_res.is_changed() {
|
||||
return;
|
||||
@@ -86,28 +144,29 @@ fn update_status(phase_res: Res<CurrentPhase>, mut text_query: Query<(&mut Text,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles HUD button interactions.
|
||||
fn buttons(
|
||||
mut commands: Commands,
|
||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||
root_query: Query<(Entity, &RootMarker)>,
|
||||
mut savegame_messages: MessageWriter<SavegameDumpMessage>,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut timer_settings: ResMut<TimerSettings>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
achievement_progress: Res<AchievementProgress>,
|
||||
root_query: Query<(Entity, &RootMarker)>,
|
||||
) {
|
||||
let shift_multiplier = if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
||||
10
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
for (interaction, button_type) in &mut interaction_query {
|
||||
match *interaction {
|
||||
Interaction::Pressed => match button_type {
|
||||
ButtonType::SettingsOpen => {
|
||||
open_settings(&mut commands);
|
||||
}
|
||||
ButtonType::SettingsClose => {
|
||||
for (entity, root) in root_query.iter() {
|
||||
match *root {
|
||||
RootMarker::Settings => commands.entity(entity).despawn(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ButtonType::SettingsExit => {
|
||||
savegame_messages.write(SavegameDumpMessage);
|
||||
next_state.set(AppState::StartScreen);
|
||||
@@ -115,12 +174,20 @@ fn buttons(
|
||||
ButtonType::SettingsSave => {
|
||||
savegame_messages.write(SavegameDumpMessage);
|
||||
}
|
||||
ButtonType::SettingsAchievements => {
|
||||
open_achievements_menu(&mut commands, &achievement_progress);
|
||||
for (entity, root) in root_query.iter() {
|
||||
if let RootMarker::Settings = root {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
ButtonType::SettingsTimerChange { input, amount } => match input {
|
||||
SettingsTimerInput::Minutes(timer_type) => {
|
||||
timer_settings.change(timer_type, 60 * amount)
|
||||
timer_settings.change(timer_type, 60 * amount * shift_multiplier)
|
||||
}
|
||||
SettingsTimerInput::Seconds(timer_type) => {
|
||||
timer_settings.change(timer_type, *amount)
|
||||
timer_settings.change(timer_type, *amount * shift_multiplier)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -129,12 +196,14 @@ fn buttons(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up HUD resources.
|
||||
fn cleanup(mut commands: Commands, query: Query<Entity, With<RootMarker>>) {
|
||||
for entity in query.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the timer settings display in the settings menu.
|
||||
fn update_timer_settings(
|
||||
timer_settings: ResMut<TimerSettings>,
|
||||
mut query: Query<(&SettingsTimerInput, &mut Text)>,
|
||||
@@ -162,3 +231,22 @@ fn update_timer_settings(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the visibility of the shovel overlay based on inventory.
|
||||
fn update_shovel_overlay_visibility(
|
||||
inventory: Res<inventory::components::Inventory>,
|
||||
item_stacks: Query<&inventory::components::ItemStack>,
|
||||
mut overlay_query: Query<(&RootMarker, &mut Visibility)>,
|
||||
) {
|
||||
let has_shovel = inventory.has_item_type(&item_stacks, inventory::components::ItemType::Shovel);
|
||||
|
||||
for (marker, mut vis) in overlay_query.iter_mut() {
|
||||
if let RootMarker::ShovelOverlay = marker {
|
||||
*vis = if has_shovel {
|
||||
Visibility::Inherited
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,76 +2,38 @@ use super::super::components::*;
|
||||
use super::timer_settings::timer_settings;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Spawns the settings popup.
|
||||
pub fn open_settings(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
spawn_popup(
|
||||
commands,
|
||||
RootMarker::Settings,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..Node::center()
|
||||
},
|
||||
ZIndex(1),
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
"Spiel Einstellungen",
|
||||
Node {
|
||||
width: px(700),
|
||||
padding: UiRect::all(px(20.0)),
|
||||
..Node::vstack(px(20))
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||
BorderRadius::all(px(10.0)),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
justify_content: JustifyContent::SpaceBetween,
|
||||
..Node::hstack(px(20))
|
||||
},
|
||||
children![
|
||||
text("Spiel Einstellungen", 40.0, Color::WHITE),
|
||||
pill_button(
|
||||
ButtonType::SettingsClose,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..default()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
parent
|
||||
.spawn(Node::vstack(px(10)))
|
||||
.with_children(|parent| {
|
||||
parent.spawn(button(
|
||||
|parent| {
|
||||
parent.spawn((
|
||||
Node::vstack(px(10)),
|
||||
children![
|
||||
button(
|
||||
ButtonType::SettingsExit,
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
padding: UiRect::all(px(10)),
|
||||
..default()
|
||||
},
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Spiel verlassen", 24.0, color)
|
||||
));
|
||||
|
||||
parent.spawn(button(
|
||||
),
|
||||
button(
|
||||
ButtonType::SettingsSave,
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
padding: UiRect::all(px(10)),
|
||||
..default()
|
||||
},
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Spiel speichern", 24.0, color)
|
||||
));
|
||||
|
||||
parent.spawn((
|
||||
),
|
||||
button(
|
||||
ButtonType::SettingsAchievements,
|
||||
ButtonVariant::Secondary,
|
||||
Node::from_padding(UiRect::all(px(10))),
|
||||
|color| text("Erfolge", 24.0, color)
|
||||
),(
|
||||
Node {
|
||||
justify_content: JustifyContent::Center,
|
||||
..Node::hstack(px(30))
|
||||
@@ -122,8 +84,9 @@ pub fn open_settings(commands: &mut Commands) {
|
||||
]
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
));
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::super::components::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Creates a UI bundle for a specific timer setting.
|
||||
pub fn timer_settings(timer_type: TimerType) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::features::{
|
||||
hud::ui::settings::open_settings,
|
||||
input::utils::mouse_to_grid,
|
||||
inventory::{components::ItemStack, ui::open_inventory},
|
||||
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
|
||||
pom::messages::InvalidMoveMessage,
|
||||
shop::ui::open_shop,
|
||||
ui::{messages::ClosePopupMessage, ui::popups::PopupRoot},
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use bevy::input::mouse::MouseButton;
|
||||
@@ -10,6 +13,7 @@ use bevy::window::PrimaryWindow;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
/// Handles user input for the game.
|
||||
pub struct InputPlugin;
|
||||
|
||||
impl Plugin for InputPlugin {
|
||||
@@ -38,9 +42,17 @@ impl Plugin for InputPlugin {
|
||||
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
|
||||
|
||||
app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen)));
|
||||
app.add_systems(
|
||||
Update,
|
||||
inventory_keybind.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
|
||||
app.add_message::<ClosePopupMessage>();
|
||||
app.add_systems(Update, popup_keybind);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles right-click movement input.
|
||||
fn move_click(
|
||||
mut move_messages: MessageWriter<MoveMessage>,
|
||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||
@@ -49,7 +61,13 @@ fn move_click(
|
||||
config: Res<GameConfig>,
|
||||
phase: Res<CurrentPhase>,
|
||||
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||
inventory: Res<Inventory>,
|
||||
item_stacks: Query<&ItemStack>,
|
||||
) {
|
||||
if inventory.has_item_type(&item_stacks, ItemType::Shovel) {
|
||||
return; // Block movement if player has a shovel
|
||||
}
|
||||
|
||||
match phase.0 {
|
||||
Phase::Focus { .. } => return,
|
||||
_ => {}
|
||||
@@ -65,6 +83,7 @@ fn move_click(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles left-click interactions (tile selection, shovel).
|
||||
fn interact_click(
|
||||
mut tile_click_messages: MessageWriter<TileClickMessage>,
|
||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||
@@ -74,12 +93,25 @@ fn interact_click(
|
||||
config: Res<GameConfig>,
|
||||
phase: Res<CurrentPhase>,
|
||||
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||
mut commands: Commands,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stacks: Query<&mut ItemStack>,
|
||||
grid: Res<Grid>,
|
||||
mut tile_states: Query<&mut TileState>,
|
||||
) {
|
||||
match phase.0 {
|
||||
Phase::Focus { .. } => return,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let has_shovel = inventory.items.iter().any(|&entity| {
|
||||
if let Ok(stack) = item_stacks.get(entity) {
|
||||
stack.item_type == ItemType::Shovel
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if mouse_btn.just_pressed(MouseButton::Left) {
|
||||
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
||||
return;
|
||||
@@ -88,10 +120,71 @@ fn interact_click(
|
||||
return;
|
||||
};
|
||||
|
||||
if has_shovel {
|
||||
// Shovel interaction logic
|
||||
let tile_entity = match grid.get_tile((x, y)) {
|
||||
Ok(entity) => entity,
|
||||
Err(_) => return, // Clicked outside grid
|
||||
};
|
||||
|
||||
// Before getting mutable tile_state, check neighbors with immutable borrow
|
||||
let mut has_claimed_neighbor = false;
|
||||
// Check if not on edge first for early exit to avoid checking neighbors outside grid.
|
||||
if x == 0 || x == grid.width - 1 || y == 0 || y == grid.height - 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let neighbors = [
|
||||
(x + 1, y),
|
||||
(x.saturating_sub(1), y),
|
||||
(x, y + 1),
|
||||
(x, y.saturating_sub(1)),
|
||||
];
|
||||
|
||||
for (nx, ny) in neighbors.iter() {
|
||||
// Ensure neighbor coordinates are within grid boundaries before attempting to get tile
|
||||
if *nx < grid.width && *ny < grid.height {
|
||||
if let Ok(neighbor_entity) = grid.get_tile((*nx, *ny)) {
|
||||
if let Ok(neighbor_state) = tile_states.get(neighbor_entity) {
|
||||
if !matches!(*neighbor_state, TileState::Unclaimed) {
|
||||
has_claimed_neighbor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_claimed_neighbor {
|
||||
return; // No claimed neighbor, cannot unlock
|
||||
}
|
||||
|
||||
// Now get mutable tile_state, after all immutable neighbor checks are done
|
||||
let mut tile_state = match tile_states.get_mut(tile_entity) {
|
||||
Ok(state) => state,
|
||||
Err(_) => return, // Should not happen
|
||||
};
|
||||
|
||||
// Check if unclaimed AFTER determining neighbor status
|
||||
if !matches!(*tile_state, TileState::Unclaimed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if has_claimed_neighbor {
|
||||
// This check is redundant due to early return above, but kept for clarity
|
||||
// Unlock tile
|
||||
*tile_state = TileState::Empty;
|
||||
inventory.update_item_stack(&mut commands, &mut item_stacks, ItemType::Shovel, -1);
|
||||
}
|
||||
return; // Consume click event, prevent normal tile_click_messages
|
||||
} else {
|
||||
// Normal interaction
|
||||
tile_click_messages.write(TileClickMessage { x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles debug interactions (shift + left click).
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_click(
|
||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
@@ -138,6 +231,7 @@ fn debug_click(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pauses/resumes the phase timer on Space press.
|
||||
fn phase_timer_pause(
|
||||
mut pause_messages: MessageWriter<PhaseTimerPauseMessage>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
@@ -147,19 +241,58 @@ fn phase_timer_pause(
|
||||
}
|
||||
}
|
||||
|
||||
/// Skips to the next phase on Enter press.
|
||||
fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInput<KeyCode>>) {
|
||||
if keys.just_pressed(KeyCode::Enter) {
|
||||
messages.write(NextPhaseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the shop on 'P' press.
|
||||
fn shop_keybind(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut commands: Commands,
|
||||
game_config: Res<GameConfig>,
|
||||
asset_server: Res<AssetServer>,
|
||||
grid: Res<Grid>,
|
||||
tile_query: Query<&TileState>,
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::KeyP) {
|
||||
open_shop(&mut commands, &game_config, &asset_server);
|
||||
open_shop(
|
||||
&mut commands,
|
||||
&game_config,
|
||||
&asset_server,
|
||||
&grid,
|
||||
&tile_query,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the inventory on 'I' press.
|
||||
fn inventory_keybind(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut commands: Commands,
|
||||
item_stacks: Query<&ItemStack>,
|
||||
game_config: Res<GameConfig>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::KeyI) {
|
||||
open_inventory(&mut commands, item_stacks, &game_config, &asset_server);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes popups on Escape press or opens settings if no popup is open.
|
||||
fn popup_keybind(
|
||||
mut close_popup_messages: MessageWriter<ClosePopupMessage>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
popup_query: Query<Entity, With<PopupRoot>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if keys.just_pressed(KeyCode::Escape) {
|
||||
if !popup_query.is_empty() {
|
||||
close_popup_messages.write(ClosePopupMessage);
|
||||
} else {
|
||||
open_settings(&mut commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
/// Converts mouse position to grid coordinates, respecting UI blocks.
|
||||
pub fn mouse_to_grid(
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::features::config::components::BerrySeedConfig;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Types of items available in the game.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ItemType {
|
||||
Berry,
|
||||
@@ -80,8 +81,8 @@ impl ItemType {
|
||||
aseprite: asset_server.load("berry.aseprite"),
|
||||
},
|
||||
ItemType::Shovel => AseSlice {
|
||||
name: "Berry".into(),
|
||||
aseprite: asset_server.load("berry.aseprite"),
|
||||
name: "Shovel".into(),
|
||||
aseprite: asset_server.load("shovel.aseprite"),
|
||||
},
|
||||
ItemType::BerrySeed { name } => {
|
||||
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
|
||||
@@ -102,26 +103,32 @@ impl ItemType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Serialize, Deserialize, Clone)]
|
||||
/// A stack of items of a specific type.
|
||||
#[derive(Component, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ItemStack {
|
||||
pub item_type: ItemType,
|
||||
pub amount: u32,
|
||||
}
|
||||
|
||||
/// Resource containing all items owned by the player.
|
||||
#[derive(Resource, Default, Serialize, Deserialize)]
|
||||
pub struct Inventory {
|
||||
pub items: Vec<Entity>,
|
||||
}
|
||||
|
||||
impl Inventory {
|
||||
pub fn has_item(&self, items_query: Query<&ItemStack>) -> bool {
|
||||
self.items
|
||||
.iter()
|
||||
.map(|entity| items_query.get(*entity).ok())
|
||||
.find(|option| option.is_some())
|
||||
.is_some()
|
||||
/// Checks if the inventory contains a specific item type.
|
||||
pub fn has_item_type(&self, items_query: &Query<&ItemStack>, item_type: ItemType) -> bool {
|
||||
self.items.iter().any(|&entity| {
|
||||
if let Ok(stack) = items_query.get(entity) {
|
||||
stack.item_type == item_type
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds or removes items from the inventory.
|
||||
pub fn update_item_stack(
|
||||
&mut self,
|
||||
commands: &mut Commands,
|
||||
@@ -191,13 +198,14 @@ impl Inventory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Markers for inventory UI root nodes.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
Inventory,
|
||||
}
|
||||
|
||||
/// Markers for inventory-related buttons.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
InventoryOpen,
|
||||
InventoryClose,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#[cfg(debug_assertions)]
|
||||
use crate::features::phase::components::SessionTracker;
|
||||
use crate::{features::inventory::ui::open_inventory, prelude::*};
|
||||
use components::*;
|
||||
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin for the inventory system, including storage and UI.
|
||||
pub struct InventoryPlugin;
|
||||
|
||||
impl Plugin for InventoryPlugin {
|
||||
@@ -17,11 +20,11 @@ impl Plugin for InventoryPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles inventory button interactions.
|
||||
fn buttons(
|
||||
mut commands: Commands,
|
||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||
itemstack_query: Query<&ItemStack>,
|
||||
root_query: Query<(Entity, &RootMarker)>,
|
||||
game_config: Res<GameConfig>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
@@ -31,30 +34,27 @@ fn buttons(
|
||||
ButtonType::InventoryOpen => {
|
||||
open_inventory(&mut commands, itemstack_query, &game_config, &asset_server);
|
||||
}
|
||||
ButtonType::InventoryClose => {
|
||||
for (entity, root) in root_query.iter() {
|
||||
match *root {
|
||||
RootMarker::Inventory => commands.entity(entity).despawn(),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug system to add/remove berries with arrow keys.
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_modify_berries(
|
||||
mut commands: Commands,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut items: Query<&mut ItemStack>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
mut session_tracker: ResMut<SessionTracker>,
|
||||
) {
|
||||
if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
||||
if keys.just_pressed(KeyCode::ArrowUp) {
|
||||
println!("Adding 1 berry using debug bind");
|
||||
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, 1);
|
||||
if inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, 1) {
|
||||
session_tracker.total_berries_earned += 1;
|
||||
}
|
||||
} else if keys.just_pressed(KeyCode::ArrowDown) {
|
||||
println!("Removing 1 berry using debug bind");
|
||||
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, -1);
|
||||
|
||||
@@ -1,59 +1,20 @@
|
||||
use super::super::components::{ButtonType, RootMarker};
|
||||
use super::super::components::RootMarker;
|
||||
use crate::prelude::GameConfig;
|
||||
use crate::{features::inventory::ui::list_itemstack, prelude::*};
|
||||
|
||||
/// Spawns the inventory popup.
|
||||
pub fn open_inventory(
|
||||
commands: &mut Commands,
|
||||
items: Query<&ItemStack>,
|
||||
game_config: &Res<GameConfig>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
) {
|
||||
commands
|
||||
.spawn((
|
||||
spawn_popup(
|
||||
commands,
|
||||
RootMarker::Inventory,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..Node::center()
|
||||
},
|
||||
ZIndex(1),
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
Node {
|
||||
padding: UiRect::all(px(20.0)),
|
||||
..Node::vstack(px(0))
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||
BorderRadius::all(px(10.0)),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
width: percent(100.0),
|
||||
justify_content: JustifyContent::SpaceBetween,
|
||||
margin: UiRect::bottom(px(20.0)),
|
||||
..Node::hstack(px(20))
|
||||
},
|
||||
children![
|
||||
text("Inventar", 40.0, Color::WHITE),
|
||||
pill_button(
|
||||
ButtonType::InventoryClose,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..default()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
"Inventar",
|
||||
Node::default(),
|
||||
|parent| {
|
||||
parent
|
||||
.spawn(Node {
|
||||
width: percent(100),
|
||||
@@ -61,10 +22,13 @@ pub fn open_inventory(
|
||||
..Node::vstack(px(10))
|
||||
})
|
||||
.with_children(|parent| {
|
||||
for itemstack in items.iter() {
|
||||
parent.spawn(list_itemstack(itemstack, game_config, asset_server));
|
||||
}
|
||||
});
|
||||
items
|
||||
.iter()
|
||||
.map(|item| list_itemstack(item, game_config, asset_server))
|
||||
.for_each(|itemstack| {
|
||||
parent.spawn(itemstack);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Creates a UI bundle for a single item stack in the inventory list.
|
||||
pub fn list_itemstack(
|
||||
itemstack: &ItemStack,
|
||||
game_config: &GameConfig,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod achievement;
|
||||
pub mod config;
|
||||
pub mod core;
|
||||
pub mod game_screen;
|
||||
@@ -5,22 +6,27 @@ pub mod grid;
|
||||
pub mod hud;
|
||||
pub mod input;
|
||||
pub mod inventory;
|
||||
pub mod notification;
|
||||
pub mod phase;
|
||||
pub mod pom;
|
||||
pub mod savegame;
|
||||
pub mod shop;
|
||||
pub mod start_screen;
|
||||
pub mod ui;
|
||||
pub mod wonderevent;
|
||||
|
||||
pub use achievement::AchievementPlugin;
|
||||
pub use core::CorePlugin;
|
||||
pub use game_screen::GameScreenPlugin;
|
||||
pub use grid::GridPlugin;
|
||||
pub use hud::HudPlugin;
|
||||
pub use input::InputPlugin;
|
||||
pub use inventory::InventoryPlugin;
|
||||
pub use notification::NotificationPlugin;
|
||||
pub use phase::PhasePlugin;
|
||||
pub use pom::PomPlugin;
|
||||
pub use savegame::SavegamePlugin;
|
||||
pub use shop::ShopPlugin;
|
||||
pub use start_screen::StartScreenPlugin;
|
||||
pub use ui::UiPlugin;
|
||||
pub use wonderevent::WonderEventPlugin;
|
||||
|
||||
79
src/features/notification/components.rs
Normal file
79
src/features/notification/components.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Resource managing active notifications.
|
||||
#[derive(Resource)]
|
||||
pub struct Notifications {
|
||||
items: Vec<(Notification, NotificationLevel)>,
|
||||
}
|
||||
|
||||
impl Default for Notifications {
|
||||
fn default() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Notifications {
|
||||
fn new(
|
||||
&mut self,
|
||||
level: NotificationLevel,
|
||||
title: Option<impl Into<String>>,
|
||||
message: impl Into<String>,
|
||||
) {
|
||||
self.items.push((
|
||||
Notification {
|
||||
title: title.map(|s| s.into()),
|
||||
message: message.into(),
|
||||
},
|
||||
level,
|
||||
));
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> std::vec::Drain<'_, (Notification, NotificationLevel)> {
|
||||
self.items.drain(..)
|
||||
}
|
||||
|
||||
pub fn info(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
|
||||
self.new(NotificationLevel::Info, title, message);
|
||||
}
|
||||
|
||||
pub fn warn(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
|
||||
self.new(NotificationLevel::Warning, title, message);
|
||||
}
|
||||
|
||||
pub fn error(&mut self, title: Option<impl Into<String>>, message: impl Into<String>) {
|
||||
self.new(NotificationLevel::Error, title, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker for the UI node containing notifications.
|
||||
#[derive(Component)]
|
||||
pub struct NotificationContainer;
|
||||
|
||||
/// Component tracking the lifetime of a displayed notification.
|
||||
#[derive(Component)]
|
||||
pub struct NotificationLifetime(pub Timer);
|
||||
|
||||
/// Data for a single notification.
|
||||
#[derive(Component)]
|
||||
pub struct Notification {
|
||||
pub title: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Severity level of a notification.
|
||||
#[derive(Component)]
|
||||
pub enum NotificationLevel {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl NotificationLevel {
|
||||
pub fn bg_color(&self) -> Color {
|
||||
match self {
|
||||
NotificationLevel::Info => Color::srgba(0.0, 0.0, 0.0, 0.7),
|
||||
NotificationLevel::Warning => Color::srgba(1.0, 1.0, 0.0, 0.7),
|
||||
NotificationLevel::Error => Color::srgba(1.0, 0.0, 0.0, 0.7),
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/features/notification/mod.rs
Normal file
59
src/features/notification/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::{
|
||||
features::notification::ui::{notification_container, spawn_notification},
|
||||
prelude::*,
|
||||
};
|
||||
use components::{NotificationContainer, NotificationLifetime, Notifications};
|
||||
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin for the notification system.
|
||||
pub struct NotificationPlugin;
|
||||
|
||||
impl Plugin for NotificationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<Notifications>()
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, handle_notification);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the notification container up.
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn(notification_container());
|
||||
}
|
||||
|
||||
/// Spawns/Despawns UI elements for each item in the `Notifications` Resource.
|
||||
fn handle_notification(
|
||||
mut commands: Commands,
|
||||
mut notifications: ResMut<Notifications>,
|
||||
container_q: Query<Entity, With<NotificationContainer>>,
|
||||
mut notification_entities: Query<(Entity, &mut NotificationLifetime)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
if let Some(container) = container_q.iter().next() {
|
||||
let mut spawned_entities = Vec::new();
|
||||
|
||||
for (content, level) in notifications.drain() {
|
||||
let entity = spawn_notification(&mut commands, content, level);
|
||||
commands.entity(container).add_child(entity);
|
||||
spawned_entities.push(entity);
|
||||
}
|
||||
|
||||
for entity in spawned_entities {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(NotificationLifetime(Timer::from_seconds(
|
||||
5.0,
|
||||
TimerMode::Once,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for (entity, mut timer) in notification_entities.iter_mut() {
|
||||
timer.0.tick(time.delta());
|
||||
if timer.0.is_finished() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/features/notification/ui.rs
Normal file
62
src/features/notification/ui.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use super::components::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Creates the notification container UI bundle.
|
||||
pub fn notification_container() -> impl Bundle {
|
||||
(
|
||||
NotificationContainer,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: px(0),
|
||||
left: px(0),
|
||||
padding: UiRect::all(px(5)),
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: px(5),
|
||||
..default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Spawns a single notification UI element.
|
||||
pub fn spawn_notification(
|
||||
commands: &mut Commands,
|
||||
content: Notification,
|
||||
level: NotificationLevel,
|
||||
) -> Entity {
|
||||
let entity = commands
|
||||
.spawn((
|
||||
Node {
|
||||
flex_direction: FlexDirection::Column,
|
||||
row_gap: px(5),
|
||||
padding: UiRect::all(px(10)),
|
||||
margin: UiRect::all(px(2)),
|
||||
width: px(400),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(level.bg_color()),
|
||||
BorderRadius::all(px(4)),
|
||||
))
|
||||
.with_children(|p| {
|
||||
if let Some(title) = content.title {
|
||||
p.spawn((
|
||||
Text::new(format!("{}\n", title)),
|
||||
TextFont {
|
||||
font_size: 20.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::WHITE),
|
||||
));
|
||||
}
|
||||
p.spawn((
|
||||
Text::new(content.message),
|
||||
TextFont {
|
||||
font_size: 16.0,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::WHITE),
|
||||
));
|
||||
})
|
||||
.id();
|
||||
|
||||
entity
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::utils::format_time;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Represents the different states of the Pomodoro timer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Phase {
|
||||
Break { duration: f32 },
|
||||
@@ -37,9 +38,11 @@ impl Phase {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource holding the current phase state.
|
||||
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CurrentPhase(pub Phase);
|
||||
|
||||
/// Configuration for phase durations.
|
||||
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TimerSettings {
|
||||
pub focus_duration: u32,
|
||||
@@ -59,7 +62,10 @@ impl Default for TimerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks statistics for the current session.
|
||||
#[derive(Resource, Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct SessionTracker {
|
||||
pub completed_focus_phases: u32,
|
||||
pub total_berries_earned: u32,
|
||||
pub total_plants_withered: u32,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Message sent when a phase timer reaches zero.
|
||||
#[derive(Message)]
|
||||
pub struct PhaseTimerFinishedMessage {
|
||||
pub phase: Phase,
|
||||
}
|
||||
|
||||
/// Message to toggle pause state.
|
||||
#[derive(Message)]
|
||||
pub struct PhaseTimerPauseMessage;
|
||||
|
||||
/// Message to proceed to the next phase.
|
||||
#[derive(Message)]
|
||||
pub struct NextPhaseMessage;
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod components;
|
||||
pub mod messages;
|
||||
pub mod utils;
|
||||
|
||||
/// Plugin managing the Pomodoro phase timer and state.
|
||||
pub struct PhasePlugin;
|
||||
|
||||
impl Plugin for PhasePlugin {
|
||||
@@ -22,7 +23,13 @@ impl Plugin for PhasePlugin {
|
||||
app.add_systems(OnEnter(AppState::GameScreen), load_rules);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(tick_timer, handle_pause, handle_continue).run_if(in_state(AppState::GameScreen)),
|
||||
(
|
||||
tick_timer,
|
||||
handle_pause,
|
||||
handle_continue,
|
||||
grant_focus_rewards,
|
||||
)
|
||||
.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -30,6 +37,7 @@ impl Plugin for PhasePlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug system to shorten phase duration for testing.
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_short_phase_duration(
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
@@ -49,6 +57,7 @@ fn debug_short_phase_duration(
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the current phase duration from settings.
|
||||
fn load_rules(mut phase_res: ResMut<CurrentPhase>, settings: Res<TimerSettings>) {
|
||||
let phase = &mut phase_res.0;
|
||||
|
||||
@@ -65,7 +74,10 @@ fn load_rules(mut phase_res: ResMut<CurrentPhase>, settings: Res<TimerSettings>)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ticks the phase timer and handles completion.
|
||||
fn tick_timer(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
time: Res<Time>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>,
|
||||
@@ -87,7 +99,11 @@ fn tick_timer(
|
||||
completed_phase: Box::new(completed),
|
||||
};
|
||||
|
||||
println!("phase ended");
|
||||
println!("Phase ended");
|
||||
commands.spawn((
|
||||
AudioPlayer::new(asset_server.load("sounds/beep.mp3")),
|
||||
PlaybackSettings::DESPAWN,
|
||||
));
|
||||
savegame_messages.write(SavegameDumpMessage);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +111,48 @@ fn tick_timer(
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewards the player at the end of a focus phase with `berries_per_focus_minute` * `focus_duration`.
|
||||
fn grant_focus_rewards(
|
||||
mut messages: MessageReader<PhaseTimerFinishedMessage>,
|
||||
config: Res<GameConfig>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut commands: Commands,
|
||||
mut items_query: Query<&mut ItemStack>,
|
||||
mut session_tracker: ResMut<SessionTracker>,
|
||||
game_config: Res<GameConfig>,
|
||||
mut notifications: ResMut<Notifications>,
|
||||
timer_settings: Res<TimerSettings>,
|
||||
) {
|
||||
for message in messages.read() {
|
||||
if matches!(message.phase, Phase::Focus { .. }) {
|
||||
let berries = config.berries_per_focus_minute
|
||||
* (timer_settings.focus_duration as f32 / 60.0).floor() as u32;
|
||||
|
||||
inventory.update_item_stack(
|
||||
&mut commands,
|
||||
&mut items_query,
|
||||
ItemType::Berry,
|
||||
berries as i32,
|
||||
);
|
||||
session_tracker.total_berries_earned += berries;
|
||||
session_tracker.completed_focus_phases += 1;
|
||||
|
||||
let berries_name = match berries {
|
||||
1 => ItemType::Berry.singular(&game_config),
|
||||
_ => ItemType::Berry.plural(&game_config),
|
||||
};
|
||||
notifications.info(
|
||||
Some("Fokus Belohnung"),
|
||||
format!(
|
||||
"Du hast {} {} als Belohnung für das Abschließen einer Fokus-Phase erhalten!",
|
||||
berries, berries_name
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggles pause state of the timer.
|
||||
fn handle_pause(
|
||||
mut messages: MessageReader<PhaseTimerPauseMessage>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
@@ -119,16 +177,15 @@ fn handle_pause(
|
||||
}
|
||||
}
|
||||
|
||||
/// Transitions to the next phase based on current state.
|
||||
pub fn next_phase(
|
||||
current_phase: &mut CurrentPhase,
|
||||
session_tracker: &mut SessionTracker,
|
||||
session_tracker: &SessionTracker,
|
||||
settings: &TimerSettings,
|
||||
) {
|
||||
if let Phase::Finished { completed_phase } = ¤t_phase.0 {
|
||||
match **completed_phase {
|
||||
Phase::Focus { .. } => {
|
||||
session_tracker.completed_focus_phases += 1;
|
||||
|
||||
let is_long_break = session_tracker.completed_focus_phases > 0
|
||||
&& session_tracker.completed_focus_phases % settings.long_break_interval == 0;
|
||||
|
||||
@@ -152,6 +209,7 @@ pub fn next_phase(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles transition to the next phase after user confirmation.
|
||||
pub fn handle_continue(
|
||||
mut messages: MessageReader<NextPhaseMessage>,
|
||||
mut phase_res: ResMut<CurrentPhase>,
|
||||
@@ -167,7 +225,7 @@ pub fn handle_continue(
|
||||
false
|
||||
};
|
||||
|
||||
next_phase(&mut phase_res, &mut session_tracker, &settings);
|
||||
next_phase(&mut phase_res, &session_tracker, &settings);
|
||||
|
||||
if entering_break {
|
||||
println!("Growing crops and resetting watered state.");
|
||||
@@ -198,6 +256,10 @@ pub fn handle_continue(
|
||||
}
|
||||
}
|
||||
|
||||
if new_withered && !*withered {
|
||||
session_tracker.total_plants_withered += 1;
|
||||
}
|
||||
|
||||
*state = TileState::Occupied {
|
||||
seed: seed.clone(),
|
||||
watered: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/// Formats seconds into MM:SS or HH:MM:SS string.
|
||||
pub fn format_time(seconds: f32) -> String {
|
||||
let seconds = seconds.max(0.0) as u32;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
use crate::{features::phase::components::SessionTracker, prelude::*};
|
||||
|
||||
/// Actions Pom can perform on tiles.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum InteractionAction {
|
||||
Plant(ItemType),
|
||||
@@ -88,6 +89,7 @@ impl InteractionAction {
|
||||
item_stack_query: &mut Query<&mut ItemStack>,
|
||||
commands: &mut Commands,
|
||||
game_config: &GameConfig,
|
||||
session_tracker: &mut SessionTracker,
|
||||
) {
|
||||
let Ok(tile_entity) = grid.get_tile(pos) else {
|
||||
println!("Error during interaction: Couldn't get tile_entity");
|
||||
@@ -165,6 +167,7 @@ impl InteractionAction {
|
||||
ItemType::Berry,
|
||||
config.grants as i32,
|
||||
);
|
||||
session_tracker.total_berries_earned += config.grants;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@ use crate::features::pom::actions::InteractionAction;
|
||||
use crate::prelude::*;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Marker component for the main character.
|
||||
#[derive(Component)]
|
||||
pub struct Pom;
|
||||
|
||||
#[derive(Component, Serialize, Deserialize, Clone, Copy)]
|
||||
/// Current logical position on the grid.
|
||||
#[derive(Component, Serialize, Deserialize, Clone, Copy, Default)]
|
||||
pub struct GridPosition {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
/// Queue of grid positions to visit.
|
||||
#[derive(Component, Default)]
|
||||
pub struct PathQueue {
|
||||
pub steps: VecDeque<(u32, u32)>,
|
||||
}
|
||||
|
||||
/// Movement direction state for animation.
|
||||
#[derive(Component, Default)]
|
||||
pub enum MovingState {
|
||||
#[default]
|
||||
@@ -38,6 +42,7 @@ impl MovingState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Target tile and action for pending interaction.
|
||||
#[derive(Component, Default)]
|
||||
pub struct InteractionTarget {
|
||||
pub target: Option<(u32, u32)>,
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use crate::features::pom::actions::InteractionAction;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Request to move Pom to a specific tile.
|
||||
#[derive(Message)]
|
||||
pub struct MoveMessage {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
/// Notification that a move request was invalid.
|
||||
#[derive(Message)]
|
||||
pub struct InvalidMoveMessage {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Request to start an interaction sequence.
|
||||
#[derive(Message)]
|
||||
pub struct InteractStartMessage {
|
||||
pub x: u32,
|
||||
@@ -19,6 +22,7 @@ pub struct InteractStartMessage {
|
||||
pub action: InteractionAction,
|
||||
}
|
||||
|
||||
/// Notification that a tile was clicked (requesting context menu).
|
||||
#[derive(Message)]
|
||||
pub struct TileClickMessage {
|
||||
pub x: u32,
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod messages;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
/// Plugin controlling the main character (Pom) behavior.
|
||||
pub struct PomPlugin;
|
||||
|
||||
impl Plugin for PomPlugin {
|
||||
@@ -34,6 +35,7 @@ impl Plugin for PomPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the path Pom will follow using gizmos.
|
||||
fn draw_path(
|
||||
mut gizmos: Gizmos,
|
||||
query: Query<(&Transform, &PathQueue), With<Pom>>,
|
||||
@@ -65,6 +67,7 @@ fn draw_path(
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the Pom character.
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
||||
commands.spawn((
|
||||
Pom,
|
||||
@@ -87,12 +90,14 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<Gam
|
||||
));
|
||||
}
|
||||
|
||||
/// Despawns the Pom character.
|
||||
fn cleanup(mut commands: Commands, pom_query: Query<Entity, With<Pom>>) {
|
||||
for pom_entity in pom_query.iter() {
|
||||
commands.entity(pom_entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates path for manual movement requests.
|
||||
fn handle_move(
|
||||
mut move_messages: MessageReader<MoveMessage>,
|
||||
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
||||
@@ -127,6 +132,7 @@ fn handle_move(
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates path to interaction target.
|
||||
fn handle_interact(
|
||||
mut interact_messages: MessageReader<InteractStartMessage>,
|
||||
mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>,
|
||||
@@ -184,7 +190,8 @@ fn handle_interact(
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_interaction(
|
||||
/// Executes the pending interaction when target is reached.
|
||||
pub fn perform_interaction(
|
||||
mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>,
|
||||
grid: Res<Grid>,
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
@@ -192,6 +199,8 @@ fn perform_interaction(
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
mut commands: Commands,
|
||||
config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<crate::features::phase::components::SessionTracker>,
|
||||
mut notifications: ResMut<Notifications>,
|
||||
) {
|
||||
for (pos, mut target_component, path_queue) in pom_query.iter_mut() {
|
||||
if let Some(target) = target_component.target {
|
||||
@@ -201,6 +210,17 @@ fn perform_interaction(
|
||||
}
|
||||
|
||||
if manhattan_distance(pos.x, pos.y, target.0, target.1) == 1 {
|
||||
if let Some(actions::InteractionAction::Plant(_)) = &target_component.action {
|
||||
if utils::is_trapped((pos.x, pos.y), target, &grid, |e| {
|
||||
tile_query.get(e).ok().cloned()
|
||||
}) {
|
||||
notifications.error(None::<String>, "That would trap you!");
|
||||
target_component.target = None;
|
||||
target_component.action = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Performing interaction on tile ({}, {})",
|
||||
target.0, target.1
|
||||
@@ -215,6 +235,7 @@ fn perform_interaction(
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -225,6 +246,7 @@ fn perform_interaction(
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the Pom character along the path.
|
||||
fn move_pom(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(
|
||||
@@ -274,6 +296,7 @@ fn move_pom(
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates Pom animation based on movement state.
|
||||
fn update_pom(asset_server: Res<AssetServer>, mut query: Query<(&MovingState, &mut AseAnimation)>) {
|
||||
for (moving_state, mut animation) in query.iter_mut() {
|
||||
match moving_state {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use crate::features::pom::actions::InteractionAction;
|
||||
use crate::features::ui::messages::ClosePopupMessage;
|
||||
use crate::features::ui::utils::ui_blocks;
|
||||
use crate::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
/// Marker for context menu UI root.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
ContextMenu,
|
||||
}
|
||||
|
||||
/// Buttons available in the context menu.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
Interact {
|
||||
@@ -18,7 +21,8 @@ pub enum ButtonType {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub fn spawn_context_menu(
|
||||
/// Spawns the context menu at the clicked tile position.
|
||||
pub fn open_context_menu(
|
||||
mut commands: Commands,
|
||||
mut tile_click_messages: MessageReader<TileClickMessage>,
|
||||
root_query: Query<Entity, With<RootMarker>>,
|
||||
@@ -56,22 +60,11 @@ pub fn spawn_context_menu(
|
||||
let options =
|
||||
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: px(screen_pos.x),
|
||||
top: px(screen_pos.y),
|
||||
padding: UiRect::all(px(5.0)),
|
||||
..Node::vstack(px(5.0))
|
||||
},
|
||||
ZIndex(100),
|
||||
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
|
||||
BorderRadius::all(px(5)),
|
||||
spawn_context_menu(
|
||||
&mut commands,
|
||||
RootMarker::ContextMenu,
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
screen_pos,
|
||||
|parent| {
|
||||
for option in options {
|
||||
parent.spawn(button(
|
||||
ButtonType::Interact {
|
||||
@@ -80,10 +73,7 @@ pub fn spawn_context_menu(
|
||||
action: option.clone(),
|
||||
},
|
||||
ButtonVariant::Primary,
|
||||
Node {
|
||||
padding: UiRect::all(px(5)),
|
||||
..default()
|
||||
},
|
||||
Node::from_padding(UiRect::all(px(5))),
|
||||
|c| text(option.clone().get_name(&game_config), 20.0, c), // TODO: add sprite
|
||||
));
|
||||
}
|
||||
@@ -91,17 +81,16 @@ pub fn spawn_context_menu(
|
||||
parent.spawn(button(
|
||||
ButtonType::Cancel,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
padding: UiRect::all(px(5)),
|
||||
..default()
|
||||
},
|
||||
Node::from_padding(UiRect::all(px(5))),
|
||||
|c| text("Abbrechen", 20.0, c),
|
||||
));
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes context menu when clicking elsewhere.
|
||||
pub fn click_outside_context_menu(
|
||||
mut commands: Commands,
|
||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||
@@ -122,6 +111,7 @@ pub fn click_outside_context_menu(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles context menu button clicks.
|
||||
pub fn buttons(
|
||||
mut commands: Commands,
|
||||
mut button_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||
@@ -147,3 +137,16 @@ pub fn buttons(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes context menu on ClosePopupMessage.
|
||||
pub fn close_context_menu(
|
||||
mut commands: Commands,
|
||||
mut close_popup_reader: MessageReader<ClosePopupMessage>,
|
||||
root_query: Query<Entity, With<RootMarker>>,
|
||||
) {
|
||||
for _ in close_popup_reader.read() {
|
||||
for entity in root_query.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ use crate::prelude::*;
|
||||
|
||||
pub mod context_menu;
|
||||
|
||||
/// Plugin for Pom-related UI (context menu).
|
||||
pub struct PomUiPlugin;
|
||||
|
||||
impl Plugin for PomUiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
context_menu::spawn_context_menu.run_if(in_state(AppState::GameScreen)),
|
||||
context_menu::open_context_menu.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
@@ -18,5 +19,9 @@ impl Plugin for PomUiPlugin {
|
||||
Update,
|
||||
context_menu::buttons.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
context_menu::close_context_menu.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::prelude::*;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{BinaryHeap, HashMap, VecDeque};
|
||||
use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
|
||||
|
||||
/// A star pathfinding node.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub struct Node {
|
||||
x: u32,
|
||||
@@ -25,10 +26,81 @@ impl PartialOrd for Node {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates Manhattan distance between two points.
|
||||
pub fn manhattan_distance(x1: u32, y1: u32, x2: u32, y2: u32) -> u32 {
|
||||
x1.abs_diff(x2) + y1.abs_diff(y2)
|
||||
}
|
||||
|
||||
/// Checks if Pom would be trapped in a small, isolated area if a specific tile is blocked.
|
||||
pub fn is_trapped<F>(
|
||||
start: (u32, u32),
|
||||
blocked_pos: (u32, u32),
|
||||
grid: &Grid,
|
||||
get_tile_state: F,
|
||||
) -> bool
|
||||
where
|
||||
F: Fn(Entity) -> Option<TileState>,
|
||||
{
|
||||
let mut open_set = VecDeque::new();
|
||||
let mut visited = HashSet::new();
|
||||
|
||||
open_set.push_back(start);
|
||||
visited.insert(start);
|
||||
|
||||
if start == blocked_pos {
|
||||
return true;
|
||||
}
|
||||
|
||||
let max_search = 50;
|
||||
let min_safe_area = 5;
|
||||
|
||||
while let Some(current) = open_set.pop_front() {
|
||||
if visited.len() >= max_search {
|
||||
return false;
|
||||
}
|
||||
|
||||
let neighbors = [
|
||||
(current.0 as i32 + 1, current.1 as i32),
|
||||
(current.0 as i32 - 1, current.1 as i32),
|
||||
(current.0 as i32, current.1 as i32 + 1),
|
||||
(current.0 as i32, current.1 as i32 - 1),
|
||||
];
|
||||
|
||||
for (nx, ny) in neighbors {
|
||||
if nx < 0 || ny < 0 {
|
||||
continue;
|
||||
}
|
||||
let next_pos = (nx as u32, ny as u32);
|
||||
|
||||
if next_pos == blocked_pos {
|
||||
continue;
|
||||
}
|
||||
|
||||
if visited.contains(&next_pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tile_entity = match grid.get_tile(next_pos) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(state) = get_tile_state(tile_entity) {
|
||||
if let TileState::Unclaimed = state {
|
||||
return false;
|
||||
}
|
||||
if !state.is_blocking() {
|
||||
visited.insert(next_pos);
|
||||
open_set.push_back(next_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visited.len() < min_safe_area
|
||||
}
|
||||
|
||||
/// Finds a path from start to end using A* algorithm.
|
||||
pub fn find_path(
|
||||
start: (u32, u32),
|
||||
end: (u32, u32),
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
use crate::features::achievement::components::AchievementProgress;
|
||||
use crate::prelude::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Resource containing the path to the current save file.
|
||||
#[derive(Resource, Clone, Debug)]
|
||||
pub struct SavegamePath(pub PathBuf);
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Metadata about a savegame.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SavegameInfo {
|
||||
pub path: SavegamePath,
|
||||
pub index: u32,
|
||||
pub total_berries: u32,
|
||||
pub completed_focus: u32,
|
||||
pub achievement_progress: AchievementProgress,
|
||||
}
|
||||
|
||||
/// Helper for partial JSON deserialization.
|
||||
#[derive(Deserialize)]
|
||||
struct PartialSaveData {
|
||||
session_tracker: PartialSessionTracker,
|
||||
achievement_progress: AchievementProgress,
|
||||
}
|
||||
|
||||
/// Helper for partial JSON deserialization of session stats.
|
||||
#[derive(Deserialize)]
|
||||
struct PartialSessionTracker {
|
||||
completed_focus_phases: u32,
|
||||
#[serde(default)]
|
||||
total_berries_earned: u32,
|
||||
}
|
||||
|
||||
impl SavegamePath {
|
||||
/// Constructs a new path for a specific save index.
|
||||
pub fn new(index: u32) -> Self {
|
||||
let base_path = get_internal_path().unwrap_or_else(|| {
|
||||
println!(
|
||||
@@ -39,6 +49,7 @@ impl SavegamePath {
|
||||
Self(base_path.join(format!("savegame-{}.json", index)))
|
||||
}
|
||||
|
||||
/// Lists all available savegames.
|
||||
pub fn list() -> Vec<SavegameInfo> {
|
||||
let mut savegames = Vec::new();
|
||||
|
||||
@@ -77,8 +88,9 @@ impl SavegamePath {
|
||||
savegames.push(SavegameInfo {
|
||||
path: SavegamePath(path),
|
||||
index,
|
||||
total_berries: 0, // TODO: add total_berries
|
||||
total_berries: data.session_tracker.total_berries_earned,
|
||||
completed_focus: data.session_tracker.completed_focus_phases,
|
||||
achievement_progress: data.achievement_progress,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,6 +98,7 @@ impl SavegamePath {
|
||||
savegames
|
||||
}
|
||||
|
||||
/// Returns a path for a new savegame (incremented index).
|
||||
pub fn next() -> Self {
|
||||
let savegames = Self::list();
|
||||
let next_index = savegames.last().map(|s| s.index + 1).unwrap_or(0);
|
||||
@@ -93,14 +106,16 @@ impl SavegamePath {
|
||||
}
|
||||
}
|
||||
|
||||
/// Markers for savegame UI.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
PopupSavegameLoad,
|
||||
}
|
||||
|
||||
/// Buttons for savegame management.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
SavegameLoad { savegame_path: SavegamePath },
|
||||
SavegameDelete { savegame_path: SavegamePath },
|
||||
PopupClose,
|
||||
Achievements { savegame: SavegameInfo },
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Trigger to save the current game.
|
||||
#[derive(Message)]
|
||||
pub struct SavegameDumpMessage;
|
||||
|
||||
/// Trigger to load a game from disk.
|
||||
#[derive(Message)]
|
||||
pub struct SavegameLoadMessage;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::features::achievement::components::AchievementProgress;
|
||||
use crate::features::phase::components::{SessionTracker, TimerSettings};
|
||||
use crate::features::savegame::ui::load_popup_handler;
|
||||
use crate::prelude::*;
|
||||
use components::*;
|
||||
use messages::*;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
@@ -9,6 +11,7 @@ pub mod components;
|
||||
pub mod messages;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin dealing with savegame loading and saving.
|
||||
pub struct SavegamePlugin;
|
||||
|
||||
impl Plugin for SavegamePlugin {
|
||||
@@ -18,11 +21,13 @@ impl Plugin for SavegamePlugin {
|
||||
|
||||
app.add_systems(Update, dump_savegame.run_if(in_state(AppState::GameScreen)));
|
||||
app.add_systems(Update, load_savegame.run_if(in_state(AppState::GameScreen)));
|
||||
app.add_systems(OnExit(AppState::GameScreen), reset_savegame);
|
||||
|
||||
app.add_systems(Update, load_popup_handler);
|
||||
}
|
||||
}
|
||||
|
||||
/// The structure of a save file.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SaveData {
|
||||
grid_width: u32,
|
||||
@@ -30,11 +35,13 @@ struct SaveData {
|
||||
tiles: Vec<Vec<TileState>>,
|
||||
current_phase: CurrentPhase,
|
||||
session_tracker: SessionTracker,
|
||||
achievement_progress: AchievementProgress,
|
||||
timer_settings: TimerSettings,
|
||||
pom_position: GridPosition,
|
||||
inventory: Vec<ItemStack>,
|
||||
}
|
||||
|
||||
/// Serializes game state and writes it to a file.
|
||||
fn dump_savegame(
|
||||
mut messages: MessageReader<SavegameDumpMessage>,
|
||||
save_path: Res<SavegamePath>,
|
||||
@@ -42,6 +49,7 @@ fn dump_savegame(
|
||||
tile_query: Query<&TileState>,
|
||||
phase: Res<CurrentPhase>,
|
||||
tracker: Res<SessionTracker>,
|
||||
achievement_progress: Res<AchievementProgress>,
|
||||
settings: Res<TimerSettings>,
|
||||
pom_query: Query<&GridPosition, With<Pom>>,
|
||||
inventory: Res<Inventory>,
|
||||
@@ -80,6 +88,7 @@ fn dump_savegame(
|
||||
tiles: tile_states,
|
||||
current_phase: phase.clone(),
|
||||
session_tracker: tracker.clone(),
|
||||
achievement_progress: achievement_progress.clone(),
|
||||
timer_settings: settings.clone(),
|
||||
pom_position: *pom_pos,
|
||||
inventory: item_stacks,
|
||||
@@ -104,6 +113,7 @@ fn dump_savegame(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a save file and restores game state.
|
||||
fn load_savegame(
|
||||
mut commands: Commands,
|
||||
mut messages: MessageReader<SavegameLoadMessage>,
|
||||
@@ -112,6 +122,7 @@ fn load_savegame(
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
mut phase: ResMut<CurrentPhase>,
|
||||
mut tracker: ResMut<SessionTracker>,
|
||||
mut achievement_progress: ResMut<AchievementProgress>,
|
||||
mut settings: ResMut<TimerSettings>,
|
||||
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
@@ -128,6 +139,7 @@ fn load_savegame(
|
||||
Ok(save_data) => {
|
||||
*phase = save_data.current_phase;
|
||||
*tracker = save_data.session_tracker;
|
||||
*achievement_progress = save_data.achievement_progress;
|
||||
*settings = save_data.timer_settings;
|
||||
|
||||
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
||||
@@ -171,3 +183,38 @@ fn load_savegame(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets all components/resources loaded by `load_savegame`.
|
||||
fn reset_savegame(
|
||||
mut commands: Commands,
|
||||
grid: Res<Grid>,
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
mut phase: ResMut<CurrentPhase>,
|
||||
mut tracker: ResMut<SessionTracker>,
|
||||
mut achievement_progress: ResMut<AchievementProgress>,
|
||||
mut settings: ResMut<TimerSettings>,
|
||||
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
) {
|
||||
*tracker = SessionTracker::default();
|
||||
*achievement_progress = AchievementProgress::default();
|
||||
*settings = TimerSettings::default();
|
||||
*phase = CurrentPhase(Phase::Focus {
|
||||
duration: settings.focus_duration as f32,
|
||||
});
|
||||
|
||||
inventory
|
||||
.items
|
||||
.iter()
|
||||
.for_each(|entity| commands.entity(*entity).despawn());
|
||||
inventory.items.clear();
|
||||
|
||||
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
||||
*pom_pos = GridPosition::default();
|
||||
pom_transform.translation = grid_to_world_coords(0, 0, Some(1.0), grid.width, grid.height);
|
||||
}
|
||||
|
||||
tile_query
|
||||
.iter_mut()
|
||||
.for_each(|mut state| *state = TileState::default());
|
||||
}
|
||||
|
||||
@@ -1,84 +1,49 @@
|
||||
use super::super::components::{ButtonType, RootMarker};
|
||||
use crate::features::achievement::ui::open_achievements_menu;
|
||||
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
|
||||
|
||||
pub fn spawn_load_popup(commands: &mut Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
/// Spawns the "Load Game" popup.
|
||||
pub fn spawn_load_popup(commands: &mut Commands, asset_server: &AssetServer) {
|
||||
spawn_popup(
|
||||
commands,
|
||||
RootMarker::PopupSavegameLoad,
|
||||
"Spielstand wählen",
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..Node::center()
|
||||
},
|
||||
ZIndex(1),
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
Node {
|
||||
width: px(600.0),
|
||||
height: px(500.0),
|
||||
padding: UiRect::all(px(20.0)),
|
||||
align_items: AlignItems::Center,
|
||||
..Node::vstack(px(10))
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||
BorderRadius::all(px(10.0)),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
width: percent(100.0),
|
||||
justify_content: JustifyContent::SpaceBetween,
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect::bottom(px(20.0)),
|
||||
width: px(600),
|
||||
height: px(500),
|
||||
..default()
|
||||
},
|
||||
children![
|
||||
text("Spielstand Auswahl", 40.0, Color::WHITE),
|
||||
pill_button(
|
||||
ButtonType::PopupClose,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..default()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
|parent| {
|
||||
parent
|
||||
.spawn(Node {
|
||||
width: percent(100),
|
||||
flex_direction: FlexDirection::Column,
|
||||
overflow: Overflow::scroll_y(),
|
||||
margin: UiRect::all(px(20.0)),
|
||||
row_gap: px(10.0),
|
||||
padding: UiRect::all(px(10)),
|
||||
row_gap: px(10),
|
||||
..default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
for savegame in SavegamePath::list() {
|
||||
parent.spawn(
|
||||
button(
|
||||
ButtonType::SavegameLoad { savegame_path: savegame.path.clone() },
|
||||
parent.spawn(button(
|
||||
ButtonType::SavegameLoad {
|
||||
savegame_path: savegame.path.clone(),
|
||||
},
|
||||
ButtonVariant::Secondary,
|
||||
Node {
|
||||
width: percent(100),
|
||||
padding: UiRect::all(px(10)),
|
||||
..Node::center()
|
||||
},
|
||||
|color| (
|
||||
|color| {
|
||||
(
|
||||
Node {
|
||||
width: percent(100),
|
||||
align_items: AlignItems::Center,
|
||||
..Node::hstack(px(10))
|
||||
},
|
||||
children![(
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
@@ -103,7 +68,26 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
||||
),
|
||||
]
|
||||
),
|
||||
pill_button(
|
||||
button(
|
||||
ButtonType::Achievements {
|
||||
savegame: savegame.clone()
|
||||
},
|
||||
ButtonVariant::Primary,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..default()
|
||||
},
|
||||
|_| (
|
||||
ImageNode::default(),
|
||||
AseSlice {
|
||||
aseprite: asset_server
|
||||
.load("achievement.aseprite"),
|
||||
name: "Achievement".into()
|
||||
}
|
||||
)
|
||||
),
|
||||
button(
|
||||
ButtonType::SavegameDelete {
|
||||
savegame_path: savegame.path.clone()
|
||||
},
|
||||
@@ -114,16 +98,18 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
||||
..default()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
)]
|
||||
)
|
||||
),
|
||||
);
|
||||
],
|
||||
)
|
||||
},
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles interactions in the load popup.
|
||||
pub fn load_popup_handler(
|
||||
mut commands: Commands,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
@@ -135,7 +121,6 @@ pub fn load_popup_handler(
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
match button_type {
|
||||
ButtonType::PopupClose => {}
|
||||
ButtonType::SavegameLoad { savegame_path } => {
|
||||
commands.insert_resource(savegame_path.clone());
|
||||
next_state.set(AppState::GameScreen);
|
||||
@@ -146,6 +131,9 @@ pub fn load_popup_handler(
|
||||
println!("Error while deleting savegame: {:?}", e);
|
||||
}
|
||||
}
|
||||
ButtonType::Achievements { savegame } => {
|
||||
open_achievements_menu(&mut commands, &savegame.achievement_progress);
|
||||
}
|
||||
};
|
||||
|
||||
for (entity, root) in root_query.iter() {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Markers for shop UI.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
Shop,
|
||||
}
|
||||
|
||||
/// Buttons in the shop.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
ShopOpen,
|
||||
ShopClose,
|
||||
ShopBuyItem(ShopOffer),
|
||||
}
|
||||
|
||||
/// An item available for purchase.
|
||||
#[derive(Clone)]
|
||||
pub struct ShopOffer {
|
||||
pub item: ItemStack,
|
||||
@@ -19,6 +21,7 @@ pub struct ShopOffer {
|
||||
}
|
||||
|
||||
impl ShopOffer {
|
||||
/// Generates a list of all current offers.
|
||||
pub fn list_all(game_config: &GameConfig, tile_count: u32) -> Vec<ShopOffer> {
|
||||
let mut offers = Vec::new();
|
||||
|
||||
@@ -51,6 +54,7 @@ impl ShopOffer {
|
||||
offers
|
||||
}
|
||||
|
||||
/// Attempts to purchase the offer.
|
||||
pub fn buy(
|
||||
&self,
|
||||
inventory: &mut Inventory,
|
||||
@@ -71,4 +75,3 @@ impl ShopOffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use ui::open_shop;
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin for the in-game shop.
|
||||
pub struct ShopPlugin;
|
||||
|
||||
impl Plugin for ShopPlugin {
|
||||
@@ -13,6 +14,7 @@ impl Plugin for ShopPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles shop button interactions.
|
||||
fn buttons(
|
||||
mut commands: Commands,
|
||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||
@@ -21,19 +23,14 @@ fn buttons(
|
||||
asset_server: Res<AssetServer>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut items: Query<&mut ItemStack>,
|
||||
grid: Res<Grid>,
|
||||
tile_query: Query<&TileState>,
|
||||
) {
|
||||
for (interaction, button_type) in &mut interaction_query {
|
||||
match *interaction {
|
||||
Interaction::Pressed => match button_type {
|
||||
ButtonType::ShopOpen => {
|
||||
open_shop(&mut commands, &game_config, &asset_server);
|
||||
}
|
||||
ButtonType::ShopClose => {
|
||||
for (entity, root) in root_query.iter() {
|
||||
match *root {
|
||||
RootMarker::Shop => commands.entity(entity).despawn(),
|
||||
}
|
||||
}
|
||||
open_shop(&mut commands, &game_config, &asset_server, &grid, &tile_query);
|
||||
}
|
||||
ButtonType::ShopBuyItem(offer) => {
|
||||
if offer.buy(&mut inventory, &mut commands, &mut items) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::super::components::*;
|
||||
use crate::{features::inventory::ui::item::list_itemstack, prelude::*};
|
||||
|
||||
/// Creates the UI bundle for a shop offer.
|
||||
pub fn shop_offer(
|
||||
offer: &ShopOffer,
|
||||
game_config: &GameConfig,
|
||||
@@ -26,6 +27,7 @@ pub fn shop_offer(
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates the UI bundle for displaying a price.
|
||||
pub fn shop_price(
|
||||
price: u32,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
@@ -1,64 +1,31 @@
|
||||
use super::super::components::*;
|
||||
use crate::{features::shop::ui::shop_offer, prelude::*};
|
||||
|
||||
/// Spawns the shop popup.
|
||||
pub fn open_shop(
|
||||
commands: &mut Commands,
|
||||
game_config: &GameConfig,
|
||||
asset_server: &Res<AssetServer>,
|
||||
grid: &Grid,
|
||||
tile_query: &Query<&TileState>,
|
||||
) {
|
||||
// TODO: calculate tile_count
|
||||
let offers = ShopOffer::list_all(game_config, 0);
|
||||
let tile_count = grid.count_claimed_tiles(tile_query);
|
||||
let offers = ShopOffer::list_all(game_config, tile_count);
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
spawn_popup(
|
||||
commands,
|
||||
RootMarker::Shop,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..Node::center()
|
||||
},
|
||||
ZIndex(1),
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
"Einkaufsladen",
|
||||
Node {
|
||||
width: px(700),
|
||||
padding: UiRect::all(px(20.0)),
|
||||
..Node::vstack(px(20))
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||
BorderRadius::all(px(10.0)),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
justify_content: JustifyContent::SpaceBetween,
|
||||
..Node::hstack(px(20))
|
||||
},
|
||||
children![
|
||||
text("Shop", 40.0, Color::WHITE),
|
||||
pill_button(
|
||||
ButtonType::ShopClose,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..default()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
),
|
||||
],
|
||||
));
|
||||
|
||||
|parent| {
|
||||
parent.spawn(Node::vstack(px(10))).with_children(|parent| {
|
||||
for offer in offers {
|
||||
parent.spawn(shop_offer(&offer, game_config, asset_server));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
use crate::features::phase::components::TimerSettings;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Markers for main menu UI.
|
||||
#[derive(Component)]
|
||||
pub enum RootMarker {
|
||||
MainMenu,
|
||||
Settings,
|
||||
}
|
||||
|
||||
/// Buttons in the main menu.
|
||||
#[derive(Component)]
|
||||
pub enum ButtonType {
|
||||
LoadGame,
|
||||
NewGame,
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug)]
|
||||
pub struct StartScreenTimerSettings(pub Option<TimerSettings>);
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
use crate::features::hud::components::{SettingsTimerInput, TimerType};
|
||||
use crate::features::phase::components::TimerSettings;
|
||||
use crate::features::savegame::ui::spawn_load_popup;
|
||||
use crate::prelude::*;
|
||||
use components::*;
|
||||
use ui::settings::open_settings_menu;
|
||||
|
||||
pub mod components;
|
||||
pub mod ui;
|
||||
|
||||
/// Plugin for the main menu screen.
|
||||
pub struct StartScreenPlugin;
|
||||
|
||||
impl Plugin for StartScreenPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<StartScreenTimerSettings>();
|
||||
app.add_systems(OnEnter(AppState::StartScreen), setup);
|
||||
app.add_systems(OnExit(AppState::StartScreen), cleanup);
|
||||
app.add_systems(Update, menu.run_if(in_state(AppState::StartScreen)));
|
||||
app.add_systems(
|
||||
Update,
|
||||
(menu, handle_settings_buttons, update_timer_settings_display)
|
||||
.run_if(in_state(AppState::StartScreen)),
|
||||
);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
apply_start_screen_settings.run_if(in_state(AppState::GameScreen)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the main menu UI.
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
RootMarker::MainMenu,
|
||||
@@ -60,32 +75,111 @@ fn setup(mut commands: Commands) {
|
||||
));
|
||||
}
|
||||
|
||||
/// Handles main menu button interactions.
|
||||
fn menu(
|
||||
mut commands: Commands,
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
for (interaction, button_type) in &mut interaction_query {
|
||||
match *interaction {
|
||||
Interaction::Pressed => {
|
||||
match button_type {
|
||||
Interaction::Pressed => match button_type {
|
||||
ButtonType::LoadGame => {
|
||||
spawn_load_popup(&mut commands);
|
||||
spawn_load_popup(&mut commands, &asset_server);
|
||||
}
|
||||
ButtonType::NewGame => {
|
||||
commands.insert_resource(SavegamePath::next());
|
||||
next_state.set(AppState::GameScreen);
|
||||
}
|
||||
ButtonType::Settings => todo!(),
|
||||
};
|
||||
ButtonType::Settings => {
|
||||
open_settings_menu(&mut commands);
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up main menu resources.
|
||||
fn cleanup(mut commands: Commands, query: Query<Entity, With<RootMarker>>) {
|
||||
for entity in query.iter() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles button interactions within the settings menu.
|
||||
fn handle_settings_buttons(
|
||||
mut interaction_query: Query<
|
||||
(&Interaction, &crate::features::hud::components::ButtonType),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
mut timer_settings: ResMut<TimerSettings>,
|
||||
mut ss_timer_settings: ResMut<StartScreenTimerSettings>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
let shift_multiplier = if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
||||
10
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
for (interaction, button_type) in &mut interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
if let crate::features::hud::components::ButtonType::SettingsTimerChange {
|
||||
input,
|
||||
amount,
|
||||
} = button_type
|
||||
{
|
||||
match input {
|
||||
SettingsTimerInput::Minutes(timer_type) => {
|
||||
timer_settings.change(timer_type, 60 * amount * shift_multiplier)
|
||||
}
|
||||
SettingsTimerInput::Seconds(timer_type) => {
|
||||
timer_settings.change(timer_type, *amount * shift_multiplier)
|
||||
}
|
||||
}
|
||||
ss_timer_settings.0 = Some(timer_settings.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the timer settings display in the settings menu.
|
||||
fn update_timer_settings_display(
|
||||
timer_settings: Res<TimerSettings>,
|
||||
mut query: Query<(&SettingsTimerInput, &mut Text)>,
|
||||
) {
|
||||
for (input_type, mut text) in query.iter_mut() {
|
||||
match input_type {
|
||||
SettingsTimerInput::Minutes(timer_type) => {
|
||||
let value = match timer_type {
|
||||
TimerType::Focus => timer_settings.focus_duration,
|
||||
TimerType::ShortBreak => timer_settings.short_break_duration,
|
||||
TimerType::LongBreak => timer_settings.long_break_duration,
|
||||
} as f32;
|
||||
|
||||
text.0 = format!("{:0>2}", (value / 60.0).floor());
|
||||
}
|
||||
SettingsTimerInput::Seconds(timer_type) => {
|
||||
let value = match timer_type {
|
||||
TimerType::Focus => timer_settings.focus_duration,
|
||||
TimerType::ShortBreak => timer_settings.short_break_duration,
|
||||
TimerType::LongBreak => timer_settings.long_break_duration,
|
||||
};
|
||||
|
||||
text.0 = format!("{:0>2}", (value % 60));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the timer settings from the start screen once the game screen is entered.
|
||||
fn apply_start_screen_settings(
|
||||
mut settings: ResMut<TimerSettings>,
|
||||
mut ss_settings: ResMut<StartScreenTimerSettings>,
|
||||
) {
|
||||
if let Some(new_settings) = ss_settings.0.take() {
|
||||
*settings = new_settings;
|
||||
}
|
||||
}
|
||||
|
||||
1
src/features/start_screen/ui/mod.rs
Normal file
1
src/features/start_screen/ui/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod settings;
|
||||
71
src/features/start_screen/ui/settings.rs
Normal file
71
src/features/start_screen/ui/settings.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use crate::features::hud::components::TimerType;
|
||||
use crate::features::hud::ui::timer_settings::timer_settings;
|
||||
use crate::features::start_screen::components::RootMarker;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Spawns the settings popup for the start screen.
|
||||
pub fn open_settings_menu(commands: &mut Commands) {
|
||||
spawn_popup(
|
||||
commands,
|
||||
RootMarker::Settings,
|
||||
"Einstellungen",
|
||||
Node {
|
||||
width: px(700),
|
||||
..default()
|
||||
},
|
||||
|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
justify_content: JustifyContent::Center,
|
||||
..Node::hstack(px(30))
|
||||
},
|
||||
children![
|
||||
(
|
||||
Node {
|
||||
width: percent(40),
|
||||
..Node::vstack(px(10))
|
||||
},
|
||||
children![
|
||||
text("Timer Einstellungen", 18.0, Color::WHITE),
|
||||
text(
|
||||
"Tipp: Benutze [Umschalt] um in 10er Schritten zu inkrementieren oder dekrementieren!",
|
||||
16.0,
|
||||
Color::WHITE
|
||||
),
|
||||
]
|
||||
),
|
||||
(
|
||||
Node {
|
||||
align_items: AlignItems::Center,
|
||||
..Node::vstack(px(10))
|
||||
},
|
||||
children![
|
||||
text("Fokus Phase", 12.0, Color::WHITE),
|
||||
timer_settings(TimerType::Focus)
|
||||
]
|
||||
),
|
||||
(
|
||||
Node {
|
||||
align_items: AlignItems::Center,
|
||||
..Node::vstack(px(10))
|
||||
},
|
||||
children![
|
||||
text("Kurze Pause", 12.0, Color::WHITE),
|
||||
timer_settings(TimerType::ShortBreak)
|
||||
]
|
||||
),
|
||||
(
|
||||
Node {
|
||||
align_items: AlignItems::Center,
|
||||
..Node::vstack(px(10))
|
||||
},
|
||||
children![
|
||||
text("Lange Pause", 12.0, Color::WHITE),
|
||||
timer_settings(TimerType::LongBreak)
|
||||
]
|
||||
)
|
||||
],
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Event triggering a scroll action on an entity.
|
||||
#[derive(EntityEvent, Debug)]
|
||||
#[entity_event(propagate, auto_propagate)]
|
||||
pub struct Scroll {
|
||||
@@ -7,6 +8,7 @@ pub struct Scroll {
|
||||
pub delta: Vec2,
|
||||
}
|
||||
|
||||
/// Visual styles for buttons.
|
||||
#[derive(Component, Clone)]
|
||||
pub enum ButtonVariant {
|
||||
Primary,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Pixel height of a single scroll line.
|
||||
pub const LINE_HEIGHT: f32 = 21.0;
|
||||
/// Default background color for buttons.
|
||||
pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||
/// Background color when hovering a button.
|
||||
pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||
/// Background color when pressing a button.
|
||||
pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||
|
||||
5
src/features/ui/messages.rs
Normal file
5
src/features/ui/messages.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Message to close any open popup.
|
||||
#[derive(Message)]
|
||||
pub struct ClosePopupMessage;
|
||||
@@ -1,11 +1,13 @@
|
||||
use crate::prelude::{button::update_buttons, *};
|
||||
use crate::prelude::{button::update_buttons, popups::handle_popup_close, *};
|
||||
use bevy::{input::mouse::*, picking::hover::HoverMap};
|
||||
|
||||
pub mod components;
|
||||
pub mod consts;
|
||||
pub mod messages;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
/// Plugin for general UI behavior like scrolling and button states.
|
||||
pub struct UiPlugin;
|
||||
|
||||
impl Plugin for UiPlugin {
|
||||
@@ -14,9 +16,12 @@ impl Plugin for UiPlugin {
|
||||
app.add_observer(on_scroll_handler);
|
||||
|
||||
app.add_systems(Update, update_buttons);
|
||||
|
||||
app.add_systems(Update, handle_popup_close);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads mouse wheel events and triggers scroll actions.
|
||||
fn scroll_events(
|
||||
mut mouse_wheel_reader: MessageReader<MouseWheel>,
|
||||
hover_map: Res<HoverMap>,
|
||||
@@ -42,6 +47,7 @@ fn scroll_events(
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates scroll position based on scroll events.
|
||||
fn on_scroll_handler(
|
||||
mut scroll: On<components::Scroll>,
|
||||
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Creates a standard button UI bundle.
|
||||
pub fn button<C, R>(
|
||||
button_type: impl Component,
|
||||
variant: ButtonVariant,
|
||||
@@ -24,6 +25,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a rounded pill-shaped button UI bundle.
|
||||
pub fn pill_button<C, R>(
|
||||
button_type: impl Component,
|
||||
variant: ButtonVariant,
|
||||
@@ -48,6 +50,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates button colors based on interaction state.
|
||||
pub fn update_buttons(
|
||||
mut interaction_query: Query<
|
||||
(&Interaction, &ButtonVariant, &mut BackgroundColor),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Trait for easy flexbox layout construction.
|
||||
pub trait Flexbox {
|
||||
fn hstack(spacing: Val) -> Self;
|
||||
fn vstack(spacing: Val) -> Self;
|
||||
fn center() -> Self;
|
||||
fn from_padding(padding: UiRect) -> Self;
|
||||
}
|
||||
|
||||
impl Flexbox for Node {
|
||||
@@ -30,4 +32,11 @@ impl Flexbox for Node {
|
||||
..default()
|
||||
}
|
||||
}
|
||||
|
||||
fn from_padding(padding: UiRect) -> Self {
|
||||
Node {
|
||||
padding,
|
||||
..default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod button;
|
||||
pub mod flexbox;
|
||||
pub mod popups;
|
||||
pub mod texts;
|
||||
|
||||
pub use button::{button, pill_button};
|
||||
pub use flexbox::Flexbox;
|
||||
pub use popups::{spawn_context_menu, spawn_popup};
|
||||
pub use texts::{text, text_with_component};
|
||||
|
||||
119
src/features/ui/ui/popups.rs
Normal file
119
src/features/ui/ui/popups.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use bevy::ecs::relationship::RelatedSpawnerCommands;
|
||||
|
||||
use super::super::messages::ClosePopupMessage;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Marker for popup root nodes.
|
||||
#[derive(Component)]
|
||||
pub struct PopupRoot;
|
||||
|
||||
/// Marker for the close button in a popup.
|
||||
#[derive(Component)]
|
||||
pub struct PopupCloseButton;
|
||||
|
||||
/// Spawns a generic popup window.
|
||||
pub fn spawn_popup(
|
||||
commands: &mut Commands,
|
||||
root: impl Component,
|
||||
title: impl Into<String>,
|
||||
mut node: Node,
|
||||
child: impl FnOnce(&mut RelatedSpawnerCommands<ChildOf>),
|
||||
) {
|
||||
node.flex_direction = FlexDirection::Column;
|
||||
node.row_gap = px(10);
|
||||
node.padding = UiRect::all(px(20));
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
PopupRoot,
|
||||
root,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..Node::center()
|
||||
},
|
||||
ZIndex(100),
|
||||
BackgroundColor(Color::srgba(0., 0., 0., 0.8)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent
|
||||
.spawn((
|
||||
node,
|
||||
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||
BorderRadius::all(px(10)),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
parent.spawn((
|
||||
Node {
|
||||
justify_content: JustifyContent::SpaceBetween,
|
||||
align_items: AlignItems::Center,
|
||||
..Node::hstack(px(20))
|
||||
},
|
||||
children![
|
||||
text(title, 40.0, Color::WHITE),
|
||||
button(
|
||||
PopupCloseButton,
|
||||
ButtonVariant::Destructive,
|
||||
Node {
|
||||
width: px(40),
|
||||
height: px(40),
|
||||
..Node::center()
|
||||
},
|
||||
|color| text("X", 24.0, color)
|
||||
)
|
||||
],
|
||||
));
|
||||
|
||||
child(parent);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawns a context menu at a specific position.
|
||||
pub fn spawn_context_menu(
|
||||
commands: &mut Commands,
|
||||
root: impl Component,
|
||||
position: Vec2,
|
||||
child: impl FnOnce(&mut RelatedSpawnerCommands<ChildOf>),
|
||||
) {
|
||||
commands
|
||||
.spawn((
|
||||
PopupRoot,
|
||||
root,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: px(position.x),
|
||||
top: px(position.y),
|
||||
padding: UiRect::all(px(5)),
|
||||
..Node::vstack(px(5))
|
||||
},
|
||||
ZIndex(100),
|
||||
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
|
||||
BorderRadius::all(px(5)),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(child);
|
||||
}
|
||||
|
||||
/// Handles closing popups via message or close button.
|
||||
pub fn handle_popup_close(
|
||||
mut commands: Commands,
|
||||
root_query: Query<Entity, With<PopupRoot>>,
|
||||
mut messages: MessageReader<ClosePopupMessage>,
|
||||
buttons: Query<&Interaction, (Changed<Interaction>, With<PopupCloseButton>)>,
|
||||
) {
|
||||
for _ in messages.read() {
|
||||
root_query
|
||||
.iter()
|
||||
.for_each(|root| commands.entity(root).despawn());
|
||||
}
|
||||
|
||||
buttons.iter().for_each(|i| match i {
|
||||
Interaction::Pressed => root_query
|
||||
.iter()
|
||||
.for_each(|root| commands.entity(root).despawn()),
|
||||
_ => (),
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub use crate::prelude::*;
|
||||
|
||||
/// Creates a basic text bundle.
|
||||
pub fn text(content: impl Into<String>, size: f32, color: Color) -> (Text, TextFont, TextColor) {
|
||||
(
|
||||
Text::new(content),
|
||||
@@ -8,6 +9,7 @@ pub fn text(content: impl Into<String>, size: f32, color: Color) -> (Text, TextF
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a text bundle with an additional component attached.
|
||||
pub fn text_with_component(
|
||||
component: impl Component,
|
||||
content: impl Into<String>,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
|
||||
/// Checks if the cursor is hovering over any UI element.
|
||||
pub fn ui_blocks(
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
cursor_pos: Vec2,
|
||||
|
||||
39
src/features/wonderevent/components.rs
Normal file
39
src/features/wonderevent/components.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Defines the bounds of the grid for the server.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct MaxFieldSize {
|
||||
#[serde(rename = "minX")]
|
||||
pub min_x: u32,
|
||||
#[serde(rename = "maxX")]
|
||||
pub max_x: u32,
|
||||
#[serde(rename = "minY")]
|
||||
pub min_y: u32,
|
||||
#[serde(rename = "maxY")]
|
||||
pub max_y: u32,
|
||||
}
|
||||
|
||||
/// Coordinates for a wonder event target.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Position {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
/// WebSocket messages exchanged with the wonder event server.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[serde(tag = "messageType")]
|
||||
pub enum WonderEventMessage {
|
||||
#[serde(rename = "WONDER_REQUEST")]
|
||||
WonderRequest {
|
||||
#[serde(rename = "maxFieldSize")]
|
||||
max_field_size: MaxFieldSize,
|
||||
},
|
||||
#[serde(rename = "REQUEST_ERROR")]
|
||||
RequestError { error: String },
|
||||
#[serde(rename = "NO_WONDER")]
|
||||
NoWonder,
|
||||
#[serde(rename = "WONDER_GRANTED")]
|
||||
WonderGranted { position: Position },
|
||||
}
|
||||
|
||||
267
src/features/wonderevent/mod.rs
Normal file
267
src/features/wonderevent/mod.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use self::components::{MaxFieldSize, WonderEventMessage};
|
||||
use crate::features::phase::components::{CurrentPhase, Phase, TimerSettings};
|
||||
use crate::prelude::*;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::thread;
|
||||
use tungstenite::{Message as WsMessage, connect};
|
||||
use url::Url;
|
||||
|
||||
pub mod components;
|
||||
|
||||
/// Plugin managing random wonder events via WebSocket.
|
||||
pub struct WonderEventPlugin;
|
||||
|
||||
/// Message to initiate a wonder event request.
|
||||
#[derive(Message)]
|
||||
pub struct RequestWonderEvent {
|
||||
pub url_str: String,
|
||||
pub max_field_size: MaxFieldSize,
|
||||
}
|
||||
|
||||
/// Resource holding the channel sender for wonder events.
|
||||
#[derive(Resource)]
|
||||
pub struct WonderEventSender(pub Sender<WonderEventMessage>);
|
||||
|
||||
/// Resource holding the channel receiver for wonder events.
|
||||
#[derive(Resource)]
|
||||
pub struct WonderEventReceiver(pub Mutex<Receiver<WonderEventMessage>>);
|
||||
|
||||
/// Component for animated floating text effects.
|
||||
#[derive(Component)]
|
||||
struct FloatingText {
|
||||
timer: Timer,
|
||||
velocity: Vec3,
|
||||
}
|
||||
|
||||
impl Plugin for WonderEventPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
let (tx, rx) = channel();
|
||||
app.insert_resource(WonderEventSender(tx));
|
||||
app.insert_resource(WonderEventReceiver(Mutex::new(rx)));
|
||||
|
||||
app.init_resource::<WonderEventState>();
|
||||
app.add_message::<RequestWonderEvent>();
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
check_halfway_focus.run_if(in_state(AppState::GameScreen)),
|
||||
handle_wonder_event_trigger,
|
||||
handle_wonder_event_response.run_if(in_state(AppState::GameScreen)),
|
||||
animate_floating_text,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks if an event has already triggered this phase.
|
||||
#[derive(Resource, Default)]
|
||||
struct WonderEventState {
|
||||
triggered_for_current_phase: bool,
|
||||
}
|
||||
|
||||
/// Checks if the focus phase is halfway through to trigger an event.
|
||||
fn check_halfway_focus(
|
||||
mut state: ResMut<WonderEventState>,
|
||||
phase_res: Res<CurrentPhase>,
|
||||
settings: Res<TimerSettings>,
|
||||
mut messages: MessageWriter<RequestWonderEvent>,
|
||||
config: Res<GameConfig>,
|
||||
grid: Res<Grid>,
|
||||
) {
|
||||
match &phase_res.0 {
|
||||
Phase::Focus { duration } => {
|
||||
let half_duration = settings.focus_duration as f32 / 2.0;
|
||||
if *duration <= half_duration && !state.triggered_for_current_phase {
|
||||
state.triggered_for_current_phase = true;
|
||||
println!("WonderEvent: Halfway point reached! Requesting event...");
|
||||
|
||||
let max_field_size = MaxFieldSize {
|
||||
min_x: 0,
|
||||
max_x: if grid.width > 0 { grid.width - 1 } else { 0 },
|
||||
min_y: 0,
|
||||
max_y: if grid.height > 0 { grid.height - 1 } else { 0 },
|
||||
};
|
||||
|
||||
messages.write(RequestWonderEvent {
|
||||
url_str: config.wonder_event_url.clone(),
|
||||
max_field_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
Phase::Paused { previous_phase } => {
|
||||
// If the paused phase was not Focus we can reset
|
||||
if !matches!(**previous_phase, Phase::Focus { .. }) {
|
||||
state.triggered_for_current_phase = false;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Break, Finished
|
||||
state.triggered_for_current_phase = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a thread to request a wonder event from the server.
|
||||
fn handle_wonder_event_trigger(
|
||||
mut events: MessageReader<RequestWonderEvent>,
|
||||
sender: Res<WonderEventSender>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
let url_str = event.url_str.clone();
|
||||
let max_field_size = event.max_field_size.clone();
|
||||
let tx = sender.0.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
println!("WonderEvent: Connecting to {}", url_str);
|
||||
match Url::parse(&url_str) {
|
||||
Ok(url) => match connect(url.to_string()) {
|
||||
Ok((mut socket, response)) => {
|
||||
println!("WonderEvent: Connected to the server");
|
||||
println!("Response HTTP code: {}", response.status());
|
||||
|
||||
let request = WonderEventMessage::WonderRequest { max_field_size };
|
||||
|
||||
if let Ok(json_msg) = serde_json::to_string(&request) {
|
||||
let msg = WsMessage::Text(json_msg.into());
|
||||
if let Err(e) = socket.send(msg) {
|
||||
eprintln!("WonderEvent: Error sending message: {}", e);
|
||||
} else {
|
||||
println!("WonderEvent: Request sent successfully");
|
||||
|
||||
// Read response
|
||||
if let Ok(msg) = socket.read() {
|
||||
if let Ok(text) = msg.into_text() {
|
||||
println!("WonderEvent: Received response: {}", text);
|
||||
if let Ok(response) =
|
||||
serde_json::from_str::<WonderEventMessage>(&text)
|
||||
{
|
||||
if let Err(e) = tx.send(response) {
|
||||
eprintln!(
|
||||
"WonderEvent: Failed to send response to main thread: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!("WonderEvent: Failed to parse response JSON");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("WonderEvent: Error serializing request");
|
||||
}
|
||||
|
||||
let _ = socket.close(None);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"WonderEvent: Could not connect (is the server running?): {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => eprintln!("WonderEvent: Invalid URL: {}", e),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes responses from the wonder event server.
|
||||
fn handle_wonder_event_response(
|
||||
receiver: Res<WonderEventReceiver>,
|
||||
grid: Res<Grid>,
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
game_config: Res<GameConfig>,
|
||||
mut commands: Commands,
|
||||
mut notifications: ResMut<Notifications>,
|
||||
) {
|
||||
if let Ok(rx) = receiver.0.try_lock() {
|
||||
while let Ok(msg) = rx.try_recv() {
|
||||
match msg {
|
||||
WonderEventMessage::WonderGranted { position } => {
|
||||
println!("WonderEvent: GRANTED at ({}, {})", position.x, position.y);
|
||||
|
||||
// Update Tile State
|
||||
if let Ok(_) = grid.map_tile_state(
|
||||
(position.x, position.y),
|
||||
|state| {
|
||||
if let TileState::Occupied {
|
||||
seed,
|
||||
watered,
|
||||
growth_stage,
|
||||
withered,
|
||||
dry_counter,
|
||||
} = state
|
||||
{
|
||||
// Progress growth stage
|
||||
let mut new_stage = *growth_stage;
|
||||
if let Some(config) = seed.get_seed_config(&game_config) {
|
||||
if new_stage < config.growth_stages {
|
||||
new_stage += 1;
|
||||
}
|
||||
}
|
||||
notifications.info(Some("Wunder Ereignis"), "Es ist ein Wunder passiert! Eine deiner Pflanzen ist auf magischer Weise gewachsen.");
|
||||
|
||||
TileState::Occupied {
|
||||
seed: seed.clone(),
|
||||
watered: *watered,
|
||||
growth_stage: new_stage,
|
||||
withered: *withered,
|
||||
dry_counter: *dry_counter,
|
||||
}
|
||||
} else {
|
||||
state.clone()
|
||||
}
|
||||
},
|
||||
tile_query.reborrow(),
|
||||
) {
|
||||
let world_pos = grid_to_world_coords(
|
||||
position.x,
|
||||
position.y,
|
||||
None,
|
||||
grid.width,
|
||||
grid.height,
|
||||
);
|
||||
commands.spawn((
|
||||
Text2d::new("Wunder!"),
|
||||
TextFont {
|
||||
font_size: 20.0,
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(Color::srgb(1.0, 0.8, 0.2)),
|
||||
Transform::from_translation(world_pos + Vec3::new(0.0, 20.0, 10.0)),
|
||||
FloatingText {
|
||||
timer: Timer::from_seconds(1.5, TimerMode::Once),
|
||||
velocity: Vec3::new(0.0, 20.0, 0.0),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
WonderEventMessage::RequestError { error } => {
|
||||
println!("WonderEvent: REQUEST ERROR: {}", error);
|
||||
}
|
||||
WonderEventMessage::NoWonder => {
|
||||
println!("WonderEvent: NO WONDER");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates floating text positions and lifetimes.
|
||||
fn animate_floating_text(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(Entity, &mut FloatingText, &mut Transform)>,
|
||||
) {
|
||||
for (entity, mut float, mut transform) in query.iter_mut() {
|
||||
float.timer.tick(time.delta());
|
||||
transform.translation += float.velocity * time.delta_secs();
|
||||
|
||||
if float.timer.is_finished() {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/main.rs
31
src/main.rs
@@ -1,15 +1,19 @@
|
||||
use bevy_dev_tools::fps_overlay::*;
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let config = GameConfig::read_config().unwrap_or(GameConfig::default());
|
||||
|
||||
App::new()
|
||||
.add_plugins((
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_plugins((
|
||||
DefaultPlugins.set(ImagePlugin::default_nearest()),
|
||||
AsepriteUltraPlugin,
|
||||
))
|
||||
.add_plugins((FpsOverlayPlugin {
|
||||
));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use bevy_dev_tools::fps_overlay::*;
|
||||
app.add_plugins(FpsOverlayPlugin {
|
||||
config: FpsOverlayConfig {
|
||||
refresh_interval: core::time::Duration::from_millis(100),
|
||||
enabled: true,
|
||||
@@ -20,8 +24,10 @@ fn main() {
|
||||
},
|
||||
..default()
|
||||
},
|
||||
},))
|
||||
.add_plugins((
|
||||
});
|
||||
}
|
||||
|
||||
app.add_plugins((
|
||||
features::CorePlugin,
|
||||
features::StartScreenPlugin,
|
||||
features::GameScreenPlugin,
|
||||
@@ -34,10 +40,13 @@ fn main() {
|
||||
features::UiPlugin,
|
||||
features::InventoryPlugin,
|
||||
features::ShopPlugin,
|
||||
))
|
||||
.insert_resource(config)
|
||||
.add_systems(Startup, overwrite_default_font)
|
||||
.run();
|
||||
features::WonderEventPlugin,
|
||||
features::NotificationPlugin,
|
||||
features::AchievementPlugin,
|
||||
));
|
||||
app.insert_resource(config);
|
||||
app.add_systems(Startup, overwrite_default_font);
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn overwrite_default_font(mut fonts: ResMut<Assets<Font>>) {
|
||||
|
||||
@@ -8,6 +8,7 @@ pub use crate::features::{
|
||||
utils::{grid_to_world_coords, world_to_grid_coords},
|
||||
},
|
||||
inventory::components::{Inventory, ItemStack, ItemType},
|
||||
notification::components::Notifications,
|
||||
phase::components::{CurrentPhase, Phase},
|
||||
pom::{
|
||||
components::{GridPosition, MovingState, Pom},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use directories::ProjectDirs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Returns the platform-specific data directory for the application.
|
||||
pub fn get_internal_path() -> Option<PathBuf> {
|
||||
let project_dirs = ProjectDirs::from("de", "demenik", "pomomon-garden");
|
||||
|
||||
|
||||
86
tests/achievement.rs
Normal file
86
tests/achievement.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use pomomon_garden::features::achievement::AchievementPlugin;
|
||||
use pomomon_garden::features::achievement::components::{AchievementId, AchievementProgress};
|
||||
use pomomon_garden::features::core::states::AppState;
|
||||
use pomomon_garden::features::notification::components::Notifications;
|
||||
use pomomon_garden::features::phase::components::SessionTracker;
|
||||
use pomomon_garden::prelude::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn test_achievement_unlock_logic() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.add_plugins(bevy::state::app::StatesPlugin);
|
||||
app.add_plugins(AchievementPlugin);
|
||||
app.init_state::<AppState>();
|
||||
|
||||
app.insert_resource(Notifications::default());
|
||||
app.insert_resource(SessionTracker::default());
|
||||
|
||||
{
|
||||
let mut next_state = app.world_mut().resource_mut::<NextState<AppState>>();
|
||||
next_state.set(AppState::GameScreen);
|
||||
}
|
||||
app.update();
|
||||
|
||||
// Helper to run systems and get progress
|
||||
let mut run_check = |completed_focus: u32, total_berries: u32, total_withered: u32| {
|
||||
app.insert_resource(SessionTracker {
|
||||
completed_focus_phases: completed_focus,
|
||||
total_berries_earned: total_berries,
|
||||
total_plants_withered: total_withered,
|
||||
});
|
||||
app.update(); // Runs check_achievements
|
||||
app.world().resource::<AchievementProgress>().clone()
|
||||
};
|
||||
|
||||
// --- Initial State ---
|
||||
let progress = run_check(0, 0, 0);
|
||||
for id in AchievementId::iter() {
|
||||
assert!(
|
||||
!progress.is_unlocked(&id),
|
||||
"Achievement {:?} should not be unlocked initially",
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
// --- First Steps (1 berry) ---
|
||||
let progress = run_check(0, 1, 0);
|
||||
assert!(progress.is_unlocked(&AchievementId::FirstSteps));
|
||||
|
||||
// --- Getting Started (1 focus phase) ---
|
||||
let progress = run_check(1, 1, 0);
|
||||
assert!(progress.is_unlocked(&AchievementId::GettingStarted));
|
||||
|
||||
// --- Negligent (1 withered plant) ---
|
||||
let progress = run_check(1, 1, 1);
|
||||
assert!(progress.is_unlocked(&AchievementId::Negligent));
|
||||
|
||||
// --- Focus Master (10 focus phases) ---
|
||||
let progress = run_check(10, 1, 1);
|
||||
assert!(progress.is_unlocked(&AchievementId::FocusMaster));
|
||||
|
||||
// --- Master Farmer (100 berries) ---
|
||||
let progress = run_check(10, 100, 1);
|
||||
assert!(progress.is_unlocked(&AchievementId::MasterFarmer));
|
||||
|
||||
// --- Zen Master (50 focus phases) ---
|
||||
let progress = run_check(50, 100, 1);
|
||||
assert!(progress.is_unlocked(&AchievementId::ZenMaster));
|
||||
|
||||
// --- Compost King (10 withered plants) ---
|
||||
let progress = run_check(50, 100, 10);
|
||||
assert!(progress.is_unlocked(&AchievementId::CompostKing));
|
||||
|
||||
// --- Berry Tycoon (1000 berries) ---
|
||||
let progress = run_check(50, 1000, 10);
|
||||
assert!(progress.is_unlocked(&AchievementId::BerryTycoon));
|
||||
|
||||
// --- Test idempotency: already unlocked should stay unlocked ---
|
||||
let initial_progress = progress.clone();
|
||||
let progress = run_check(50, 1000, 10);
|
||||
assert_eq!(progress.unlocked.len(), initial_progress.unlocked.len()); // Same number unlocked
|
||||
for id in AchievementId::iter() {
|
||||
assert_eq!(progress.is_unlocked(&id), initial_progress.is_unlocked(&id));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use bevy::prelude::*;
|
||||
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||
use pomomon_garden::features::phase::components::SessionTracker;
|
||||
|
||||
pub fn setup_app(
|
||||
grid_width: u32,
|
||||
@@ -65,5 +66,7 @@ pub fn setup_app(
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
app.init_resource::<SessionTracker>();
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Helper function to create a temporary file with content
|
||||
/// Helper function to create a temporary file with content
|
||||
fn create_temp_file(content: &str) -> (std::path::PathBuf, String) {
|
||||
let filename = format!("test_config_{}.json", Uuid::new_v4());
|
||||
let temp_dir = std::env::temp_dir();
|
||||
@@ -25,7 +25,9 @@ fn test_load_valid_config() {
|
||||
"pom_speed": 2.0,
|
||||
"shovel_base_price": 10,
|
||||
"shovel_rate": 0.2,
|
||||
"berry_seeds": []
|
||||
"berry_seeds": [],
|
||||
"wonder_event_url": "wss://pomomon.farm/ws",
|
||||
"berries_per_focus_minute": 1
|
||||
}"#,
|
||||
);
|
||||
|
||||
|
||||
116
tests/expansion.rs
Normal file
116
tests/expansion.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
use pomomon_garden::features::config::components::GameConfig;
|
||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
mod common;
|
||||
use common::setup_app;
|
||||
|
||||
#[test]
|
||||
fn test_shovel_expansion() {
|
||||
let mut app = setup_app(
|
||||
5,
|
||||
5,
|
||||
&[(2, 2, TileState::Empty)], // (2,2) is Empty, making it a "claimed" neighbor
|
||||
vec![(ItemType::Shovel, 1)], // One shovel in inventory
|
||||
None,
|
||||
);
|
||||
|
||||
let target_pos = (2, 3); // Unclaimed tile next to (2,2), not on edge of 5x5 grid
|
||||
|
||||
let _ = app.world_mut().run_system_once(
|
||||
move |
|
||||
grid: Res<Grid>,
|
||||
mut tile_states: Query<&mut TileState>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
mut commands: Commands,
|
||||
_game_config: Res<GameConfig> // Not directly used but required by signature
|
||||
| {
|
||||
let (x, y) = target_pos;
|
||||
|
||||
let tile_entity = match grid.get_tile((x, y)) {
|
||||
Ok(entity) => entity,
|
||||
Err(_) => panic!("Clicked outside grid at {:?}", (x,y)),
|
||||
};
|
||||
|
||||
// Mimic the edge check from interact_click
|
||||
if x == 0 || x == grid.width - 1 || y == 0 || y == grid.height - 1 {
|
||||
panic!("Should not return early due to edge check for {:?} in a 5x5 grid", target_pos);
|
||||
}
|
||||
|
||||
let neighbors = [
|
||||
(x + 1, y),
|
||||
(x.saturating_sub(1), y),
|
||||
(x, y + 1),
|
||||
(x, y.saturating_sub(1)),
|
||||
];
|
||||
|
||||
let mut has_claimed_neighbor = false;
|
||||
for (nx, ny) in neighbors.iter() {
|
||||
if *nx < grid.width && *ny < grid.height {
|
||||
if let Ok(neighbor_entity) = grid.get_tile((*nx, *ny)) {
|
||||
if let Ok(neighbor_state) = tile_states.get(neighbor_entity) {
|
||||
if !matches!(*neighbor_state, TileState::Unclaimed) {
|
||||
has_claimed_neighbor = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(has_claimed_neighbor, "Target tile {:?} should have a claimed neighbor", target_pos);
|
||||
|
||||
let mut tile_state = match tile_states.get_mut(tile_entity) {
|
||||
Ok(state) => state,
|
||||
Err(_) => panic!("Failed to get mutable tile state for {:?}", target_pos),
|
||||
};
|
||||
|
||||
assert!(matches!(*tile_state, TileState::Unclaimed), "Target tile {:?} should be Unclaimed initially, but was {:?}", target_pos, tile_state);
|
||||
|
||||
// Execute the expansion
|
||||
if inventory.update_item_stack(
|
||||
&mut commands,
|
||||
&mut item_stack_query,
|
||||
ItemType::Shovel,
|
||||
-1,
|
||||
) {
|
||||
*tile_state = TileState::Empty;
|
||||
} else {
|
||||
panic!("Shovel not consumed or not found in inventory");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.update(); // Apply commands
|
||||
|
||||
// Assert Tile State
|
||||
let grid = app.world().resource::<Grid>();
|
||||
let tile_entity = grid.get_tile(target_pos).unwrap();
|
||||
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(*tile_state, TileState::Empty),
|
||||
"Tile (1,2) should be Empty after expansion, but was {:?}",
|
||||
tile_state
|
||||
);
|
||||
|
||||
// Assert Inventory
|
||||
let inventory = app.world().resource::<Inventory>();
|
||||
let shovel_stack = inventory.items.iter().find_map(|&entity| {
|
||||
let stack = app.world().entity(entity).get::<ItemStack>()?;
|
||||
if stack.item_type == ItemType::Shovel {
|
||||
Some(stack)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
assert!(
|
||||
shovel_stack.is_none() || shovel_stack.unwrap().amount == 0,
|
||||
"Shovel should have been consumed, found {:?}",
|
||||
shovel_stack
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use bevy::ecs::system::RunSystemOnce;
|
||||
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||
use pomomon_garden::features::phase::components::SessionTracker;
|
||||
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
@@ -41,7 +42,8 @@ fn test_harvest_fully_grown() {
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
config: Res<GameConfig>| {
|
||||
config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
InteractionAction::Harvest.execute(
|
||||
(0, 0),
|
||||
&grid,
|
||||
@@ -50,6 +52,7 @@ fn test_harvest_fully_grown() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -70,6 +73,10 @@ fn test_harvest_fully_grown() {
|
||||
let stack = app.world().entity(stack_entity).get::<ItemStack>().unwrap();
|
||||
assert_eq!(stack.item_type, ItemType::Berry);
|
||||
assert_eq!(stack.amount, 5);
|
||||
|
||||
// Check Session Tracker
|
||||
let tracker = app.world().resource::<SessionTracker>();
|
||||
assert_eq!(tracker.total_berries_earned, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -105,7 +112,8 @@ fn test_harvest_withered() {
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
config: Res<GameConfig>| {
|
||||
config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
InteractionAction::Harvest.execute(
|
||||
(0, 0),
|
||||
&grid,
|
||||
@@ -114,6 +122,7 @@ fn test_harvest_withered() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -164,7 +173,8 @@ fn test_cannot_harvest_growing() {
|
||||
mut tile_query: Query<&mut TileState>,
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
config: Res<GameConfig>| {
|
||||
config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
InteractionAction::Harvest.execute(
|
||||
(0, 0),
|
||||
&grid,
|
||||
@@ -173,6 +183,7 @@ fn test_cannot_harvest_growing() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ use pomomon_garden::features::pom::utils::find_path;
|
||||
use pomomon_garden::prelude::*;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
// Helper to set up a Bevy App for pathfinding tests
|
||||
/// Helper to set up a Bevy App for pathfinding tests
|
||||
fn setup_pathfinding_app(
|
||||
grid_width: u32,
|
||||
grid_height: u32,
|
||||
|
||||
@@ -2,6 +2,7 @@ use bevy::ecs::system::RunSystemOnce;
|
||||
use pomomon_garden::features::config::components::GameConfig;
|
||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||
use pomomon_garden::features::phase::components::SessionTracker;
|
||||
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
@@ -27,7 +28,8 @@ fn test_plant_seed_interaction() {
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
mut commands: Commands,
|
||||
game_config: Res<GameConfig>| {
|
||||
game_config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
let action = InteractionAction::Plant(seed_type.clone());
|
||||
action.execute(
|
||||
(1, 1),
|
||||
@@ -37,6 +39,7 @@ fn test_plant_seed_interaction() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&game_config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -98,7 +101,8 @@ fn test_plant_seed_no_inventory() {
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
mut commands: Commands,
|
||||
game_config: Res<GameConfig>| {
|
||||
game_config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
let action = InteractionAction::Plant(seed_type.clone());
|
||||
action.execute(
|
||||
(1, 1),
|
||||
@@ -108,6 +112,7 @@ fn test_plant_seed_no_inventory() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&game_config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,9 +11,14 @@ fn test_session_tracker_focus_to_short_break() {
|
||||
}),
|
||||
});
|
||||
let timer_settings = TimerSettings::default();
|
||||
let mut session_tracker = SessionTracker::default();
|
||||
// Simulate that grant_focus_rewards has already incremented the counter
|
||||
let session_tracker = SessionTracker {
|
||||
completed_focus_phases: 1,
|
||||
total_berries_earned: 0,
|
||||
total_plants_withered: 0,
|
||||
};
|
||||
|
||||
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||
next_phase(&mut current_phase, &session_tracker, &timer_settings);
|
||||
|
||||
assert_eq!(
|
||||
session_tracker.completed_focus_phases, 1,
|
||||
@@ -37,15 +42,18 @@ fn test_session_tracker_focus_to_long_break() {
|
||||
}),
|
||||
});
|
||||
let timer_settings = TimerSettings::default();
|
||||
let mut session_tracker = SessionTracker {
|
||||
completed_focus_phases: timer_settings.long_break_interval - 1,
|
||||
}; // To trigger long break on next phase
|
||||
// Simulate that grant_focus_rewards has already incremented the counter to the interval
|
||||
let session_tracker = SessionTracker {
|
||||
completed_focus_phases: timer_settings.long_break_interval,
|
||||
total_berries_earned: 0,
|
||||
total_plants_withered: 0,
|
||||
};
|
||||
|
||||
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||
next_phase(&mut current_phase, &session_tracker, &timer_settings);
|
||||
|
||||
assert_eq!(
|
||||
session_tracker.completed_focus_phases, timer_settings.long_break_interval,
|
||||
"Completed focus phases should reach long break interval"
|
||||
"Completed focus phases should remain at long break interval"
|
||||
);
|
||||
if let Phase::Break { duration } = current_phase.0 {
|
||||
assert_eq!(
|
||||
@@ -64,12 +72,14 @@ fn test_session_tracker_break_to_focus() {
|
||||
duration: 5.0 * 60.0,
|
||||
}),
|
||||
});
|
||||
let mut session_tracker = SessionTracker {
|
||||
let session_tracker = SessionTracker {
|
||||
completed_focus_phases: 1,
|
||||
total_berries_earned: 0,
|
||||
total_plants_withered: 0,
|
||||
}; // Arbitrary value, should not change
|
||||
let timer_settings = TimerSettings::default();
|
||||
|
||||
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||
next_phase(&mut current_phase, &session_tracker, &timer_settings);
|
||||
|
||||
assert_eq!(
|
||||
session_tracker.completed_focus_phases, 1,
|
||||
@@ -89,15 +99,17 @@ fn test_session_tracker_break_to_focus() {
|
||||
fn test_session_tracker_not_finished_phase_no_change() {
|
||||
// Test that nothing changes if the phase is not `Finished`
|
||||
let mut current_phase = CurrentPhase(Phase::Focus { duration: 100.0 });
|
||||
let mut session_tracker = SessionTracker {
|
||||
let session_tracker = SessionTracker {
|
||||
completed_focus_phases: 0,
|
||||
total_berries_earned: 0,
|
||||
total_plants_withered: 0,
|
||||
};
|
||||
let timer_settings = TimerSettings::default();
|
||||
|
||||
let initial_phase = current_phase.0.clone();
|
||||
let initial_completed_focus = session_tracker.completed_focus_phases;
|
||||
|
||||
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||
next_phase(&mut current_phase, &session_tracker, &timer_settings);
|
||||
|
||||
assert_eq!(
|
||||
current_phase.0, initial_phase,
|
||||
|
||||
109
tests/trapped.rs
Normal file
109
tests/trapped.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use bevy::ecs::system::RunSystemOnce;
|
||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||
use pomomon_garden::features::inventory::components::ItemType;
|
||||
use pomomon_garden::features::notification::components::Notifications;
|
||||
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||
use pomomon_garden::features::pom::components::{GridPosition, InteractionTarget, PathQueue, Pom};
|
||||
use pomomon_garden::features::pom::perform_interaction;
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
mod common;
|
||||
use common::setup_app;
|
||||
|
||||
#[test]
|
||||
fn test_prevent_trapping_plant() {
|
||||
let occupied = TileState::Occupied {
|
||||
seed: ItemType::BerrySeed {
|
||||
name: "Wall".into(),
|
||||
},
|
||||
watered: false,
|
||||
growth_stage: 0,
|
||||
withered: false,
|
||||
dry_counter: 0,
|
||||
};
|
||||
|
||||
let mut app = setup_app(
|
||||
2,
|
||||
2,
|
||||
&[
|
||||
(0, 0, TileState::Empty), // Pom
|
||||
(1, 0, TileState::Empty), // Target
|
||||
(0, 1, occupied.clone()),
|
||||
(1, 1, occupied.clone()),
|
||||
],
|
||||
vec![(
|
||||
ItemType::BerrySeed {
|
||||
name: "Test".into(),
|
||||
},
|
||||
10,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
|
||||
app.init_resource::<Notifications>();
|
||||
|
||||
app.world_mut().spawn((
|
||||
Pom,
|
||||
GridPosition { x: 0, y: 0 },
|
||||
InteractionTarget {
|
||||
target: Some((1, 0)),
|
||||
action: Some(InteractionAction::Plant(ItemType::BerrySeed {
|
||||
name: "Test".into(),
|
||||
})),
|
||||
},
|
||||
PathQueue::default(),
|
||||
));
|
||||
|
||||
let _ = app.world_mut().run_system_once(perform_interaction);
|
||||
|
||||
let grid = app.world().resource::<Grid>();
|
||||
let tile_entity = grid.get_tile((1, 0)).unwrap();
|
||||
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||
|
||||
// Should remain Empty because planting was blocked
|
||||
match tile_state {
|
||||
TileState::Empty => (),
|
||||
_ => panic!("Should have prevented planting! State is {:?}", tile_state),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_safe_plant() {
|
||||
let mut app = setup_app(
|
||||
10,
|
||||
10,
|
||||
&[(0, 0, TileState::Empty), (1, 0, TileState::Empty)],
|
||||
vec![(
|
||||
ItemType::BerrySeed {
|
||||
name: "Test".into(),
|
||||
},
|
||||
10,
|
||||
)],
|
||||
None,
|
||||
);
|
||||
app.init_resource::<Notifications>();
|
||||
|
||||
app.world_mut().spawn((
|
||||
Pom,
|
||||
GridPosition { x: 0, y: 0 },
|
||||
InteractionTarget {
|
||||
target: Some((1, 0)),
|
||||
action: Some(InteractionAction::Plant(ItemType::BerrySeed {
|
||||
name: "Test".into(),
|
||||
})),
|
||||
},
|
||||
PathQueue::default(),
|
||||
));
|
||||
|
||||
let _ = app.world_mut().run_system_once(perform_interaction);
|
||||
|
||||
let grid = app.world().resource::<Grid>();
|
||||
let tile_entity = grid.get_tile((1, 0)).unwrap();
|
||||
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||
|
||||
// Should be Occupied
|
||||
match tile_state {
|
||||
TileState::Occupied { .. } => (),
|
||||
_ => panic!("Should have allowed planting! State is {:?}", tile_state),
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use bevy::ecs::system::RunSystemOnce;
|
||||
use pomomon_garden::features::config::components::GameConfig;
|
||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||
use pomomon_garden::features::phase::components::SessionTracker;
|
||||
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||
use pomomon_garden::prelude::*;
|
||||
|
||||
@@ -57,7 +58,8 @@ fn test_water_crop() {
|
||||
mut inventory: ResMut<Inventory>,
|
||||
mut item_stack_query: Query<&mut ItemStack>,
|
||||
mut commands: Commands,
|
||||
game_config: Res<GameConfig>| {
|
||||
game_config: Res<GameConfig>,
|
||||
mut session_tracker: ResMut<SessionTracker>| {
|
||||
let action = InteractionAction::Water;
|
||||
action.execute(
|
||||
(1, 1),
|
||||
@@ -67,6 +69,7 @@ fn test_water_crop() {
|
||||
&mut item_stack_query,
|
||||
&mut commands,
|
||||
&game_config,
|
||||
&mut session_tracker,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
176
tests/wonderevent.rs
Normal file
176
tests/wonderevent.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use bevy::prelude::*;
|
||||
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||
use pomomon_garden::features::core::states::AppState;
|
||||
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||
use pomomon_garden::features::inventory::components::ItemType;
|
||||
use pomomon_garden::features::notification::components::Notifications;
|
||||
use pomomon_garden::features::phase::components::{CurrentPhase, Phase, TimerSettings};
|
||||
use pomomon_garden::features::wonderevent::components::{Position, WonderEventMessage};
|
||||
use pomomon_garden::features::wonderevent::{
|
||||
RequestWonderEvent, WonderEventPlugin, WonderEventSender,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_wonder_event_triggers() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.add_plugins(bevy::state::app::StatesPlugin);
|
||||
app.add_plugins(WonderEventPlugin);
|
||||
|
||||
app.insert_resource(Notifications::default());
|
||||
app.insert_resource(GameConfig::default());
|
||||
app.insert_resource(TimerSettings {
|
||||
focus_duration: 60, // 60 seconds
|
||||
short_break_duration: 5,
|
||||
long_break_duration: 15,
|
||||
long_break_interval: 4,
|
||||
});
|
||||
|
||||
app.insert_resource(Grid {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: vec![],
|
||||
});
|
||||
|
||||
app.init_state::<AppState>();
|
||||
let mut next_state = app.world_mut().resource_mut::<NextState<AppState>>();
|
||||
next_state.set(AppState::GameScreen);
|
||||
|
||||
app.insert_resource(CurrentPhase(Phase::Focus { duration: 60.0 }));
|
||||
|
||||
// 1. Full Duration (60.0). No trigger.
|
||||
app.update();
|
||||
verify_event_count(&mut app, 0);
|
||||
|
||||
// 2. Half Duration (30.0). Trigger!
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Focus { duration: 30.0 });
|
||||
app.update();
|
||||
verify_event_count(&mut app, 1);
|
||||
|
||||
// 3. Less than half (29.0). Already triggered. No new trigger.
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Focus { duration: 29.0 });
|
||||
app.update();
|
||||
verify_event_count(&mut app, 0); // New events should be 0
|
||||
|
||||
// 4. Pause (Focus). Logic should NOT reset trigger.
|
||||
let prev = Box::new(Phase::Focus { duration: 29.0 });
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Paused {
|
||||
previous_phase: prev,
|
||||
});
|
||||
app.update();
|
||||
verify_event_count(&mut app, 0);
|
||||
|
||||
// 5. Resume Focus. Logic should remember it was triggered.
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Focus { duration: 29.0 });
|
||||
app.update();
|
||||
verify_event_count(&mut app, 0);
|
||||
|
||||
// 6. Break. Logic should reset trigger.
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Break { duration: 5.0 });
|
||||
app.update();
|
||||
verify_event_count(&mut app, 0);
|
||||
|
||||
// 7. New Focus (Half Duration). Logic should trigger again.
|
||||
*app.world_mut().resource_mut::<CurrentPhase>() = CurrentPhase(Phase::Focus { duration: 30.0 });
|
||||
app.update();
|
||||
verify_event_count(&mut app, 1);
|
||||
}
|
||||
|
||||
fn verify_event_count(app: &mut App, expected: usize) {
|
||||
let count = app
|
||||
.world_mut()
|
||||
.resource_mut::<Messages<RequestWonderEvent>>()
|
||||
.drain()
|
||||
.count();
|
||||
|
||||
assert_eq!(
|
||||
count, expected,
|
||||
"Expected {} events, found {}",
|
||||
expected, count
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wonder_response_handling() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.add_plugins(bevy::state::app::StatesPlugin);
|
||||
app.add_plugins(WonderEventPlugin);
|
||||
app.insert_resource(Notifications::default());
|
||||
app.insert_resource(GameConfig {
|
||||
berry_seeds: vec![BerrySeedConfig {
|
||||
name: "TestSeed".to_string(),
|
||||
cost: 1,
|
||||
grants: 1,
|
||||
slice: "Seed1".to_string(),
|
||||
growth_stages: 5,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
app.insert_resource(CurrentPhase(Phase::Focus { duration: 60.0 }));
|
||||
|
||||
// Setup Grid with an occupied tile
|
||||
let mut tiles = Vec::new();
|
||||
let column = vec![
|
||||
app.world_mut()
|
||||
.spawn((
|
||||
Tile { x: 0, y: 0 },
|
||||
TileState::Occupied {
|
||||
seed: ItemType::BerrySeed {
|
||||
name: "TestSeed".to_string(),
|
||||
},
|
||||
watered: true,
|
||||
growth_stage: 1,
|
||||
withered: false,
|
||||
dry_counter: 0,
|
||||
},
|
||||
))
|
||||
.id(),
|
||||
];
|
||||
tiles.push(column);
|
||||
app.insert_resource(Grid {
|
||||
width: 1,
|
||||
height: 1,
|
||||
tiles,
|
||||
});
|
||||
|
||||
app.insert_resource(TimerSettings::default());
|
||||
|
||||
// Init State
|
||||
app.init_state::<AppState>();
|
||||
let mut next_state = app.world_mut().resource_mut::<NextState<AppState>>();
|
||||
next_state.set(AppState::GameScreen);
|
||||
|
||||
// Run update to enter state
|
||||
app.update();
|
||||
|
||||
// Send WonderEventMessage directly to the channel
|
||||
let sender = app.world().resource::<WonderEventSender>().0.clone();
|
||||
sender
|
||||
.send(WonderEventMessage::WonderGranted {
|
||||
position: Position { x: 0, y: 0 },
|
||||
})
|
||||
.expect("Failed to send WonderEventMessage");
|
||||
|
||||
// Run update to process message
|
||||
app.update();
|
||||
|
||||
// Verify Tile State (growth should increase from 1 to 2)
|
||||
let grid = app.world().resource::<Grid>();
|
||||
let tile_entity = grid.get_tile((0, 0)).unwrap();
|
||||
let tile_state = app.world().get::<TileState>(tile_entity).unwrap();
|
||||
|
||||
if let TileState::Occupied { growth_stage, .. } = tile_state {
|
||||
assert_eq!(*growth_stage, 2, "Growth stage should increase to 2");
|
||||
} else {
|
||||
panic!("Tile state should be Occupied");
|
||||
}
|
||||
|
||||
// Verify Floating Text Spawned
|
||||
let mut text_query = app.world_mut().query::<&Text2d>();
|
||||
assert_eq!(
|
||||
text_query.iter(app.world()).count(),
|
||||
1,
|
||||
"Should spawn one floating text"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user