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",
|
"flate2",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"nom",
|
"nom",
|
||||||
"strum",
|
"strum 0.26.3",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1638,6 +1638,15 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
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]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -1892,7 +1901,7 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"core-graphics-types 0.1.3",
|
"core-graphics-types 0.1.3",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1984,6 +1993,15 @@ dependencies = [
|
|||||||
"windows 0.54.0",
|
"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]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -2029,6 +2047,16 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
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]]
|
[[package]]
|
||||||
name = "ctrlc"
|
name = "ctrlc"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -2079,6 +2107,16 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"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]]
|
[[package]]
|
||||||
name = "directories"
|
name = "directories"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -2106,6 +2144,17 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
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]]
|
[[package]]
|
||||||
name = "disqualified"
|
name = "disqualified"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -2186,6 +2235,15 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -2330,6 +2388,15 @@ dependencies = [
|
|||||||
"ttf-parser 0.20.0",
|
"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]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -2337,7 +2404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2351,12 +2418,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
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]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -2391,6 +2473,16 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"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]]
|
[[package]]
|
||||||
name = "gethostname"
|
name = "gethostname"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -2693,6 +2785,124 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
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]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.8"
|
version = "0.25.8"
|
||||||
@@ -2927,6 +3137,12 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litemap"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litrs"
|
name = "litrs"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -2999,7 +3215,7 @@ dependencies = [
|
|||||||
"bitflags 2.9.4",
|
"bitflags 2.9.4",
|
||||||
"block",
|
"block",
|
||||||
"core-graphics-types 0.2.0",
|
"core-graphics-types 0.2.0",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"log",
|
"log",
|
||||||
"objc",
|
"objc",
|
||||||
"paste",
|
"paste",
|
||||||
@@ -3075,6 +3291,23 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -3496,6 +3729,50 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
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]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3663,6 +3940,10 @@ dependencies = [
|
|||||||
"directories",
|
"directories",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"strum 0.27.2",
|
||||||
|
"strum_macros 0.27.2",
|
||||||
|
"tungstenite",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3681,6 +3962,15 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"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]]
|
[[package]]
|
||||||
name = "pp-rs"
|
name = "pp-rs"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -3913,6 +4203,7 @@ checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cpal",
|
"cpal",
|
||||||
"lewton",
|
"lewton",
|
||||||
|
"symphonia",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4019,6 +4310,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "scoped-tls"
|
name = "scoped-tls"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -4044,6 +4344,29 @@ dependencies = [
|
|||||||
"tiny-skia",
|
"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]]
|
[[package]]
|
||||||
name = "self_cell"
|
name = "self_cell"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -4099,6 +4422,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -4233,9 +4567,15 @@ version = "0.26.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -4249,6 +4589,18 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "svg_fmt"
|
name = "svg_fmt"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -4266,6 +4618,55 @@ dependencies = [
|
|||||||
"zeno",
|
"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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.106"
|
version = "2.0.106"
|
||||||
@@ -4277,6 +4678,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "sys-locale"
|
name = "sys-locale"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -4312,6 +4724,19 @@ dependencies = [
|
|||||||
"slotmap",
|
"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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@@ -4395,6 +4820,16 @@ dependencies = [
|
|||||||
"strict-num",
|
"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]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
@@ -4542,6 +4977,24 @@ version = "0.25.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
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]]
|
[[package]]
|
||||||
name = "twox-hash"
|
name = "twox-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
@@ -4554,6 +5007,12 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typewit"
|
name = "typewit"
|
||||||
version = "1.14.2"
|
version = "1.14.2"
|
||||||
@@ -4620,6 +5079,30 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
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]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.18.1"
|
version = "1.18.1"
|
||||||
@@ -4649,6 +5132,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vec_map"
|
name = "vec_map"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -5581,6 +6070,12 @@ version = "0.46.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "writeable"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11-dl"
|
name = "x11-dl"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
@@ -5650,6 +6145,29 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
|
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]]
|
[[package]]
|
||||||
name = "zeno"
|
name = "zeno"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -5675,3 +6193,57 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"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"
|
lto = "thin"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = "0.17.2"
|
bevy = { version = "0.17.2", features = ["mp3"] }
|
||||||
bevy_aseprite_ultra = "0.7.0"
|
bevy_aseprite_ultra = "0.7.0"
|
||||||
bevy_dev_tools = "0.17.2"
|
bevy_dev_tools = "0.17.2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
directories = "6.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]
|
[dev-dependencies]
|
||||||
uuid = "1.18.1"
|
uuid = "1.18.1"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ cargo run
|
|||||||
|
|
||||||
1. [Install Rust](https://rust-lang.org/tools/install/)
|
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)
|
2. [Install Bevy OS depedencies](https://bevy.org/learn/quick-start/getting-started/setup/#installing-os-dependencies)
|
||||||
|
3. Install `openssl`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://gitlab.uni-ulm.de/softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik pomomon-garden
|
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_width": 15,
|
||||||
"grid_height": 4,
|
"grid_height": 5,
|
||||||
"pom_speed": 1.5,
|
"pom_speed": 1.5,
|
||||||
"shovel_base_price": 10,
|
"shovel_base_price": 10,
|
||||||
"shovel_rate": 0.5,
|
"shovel_rate": 0.2,
|
||||||
"berry_seeds": [
|
"berry_seeds": [
|
||||||
{
|
{
|
||||||
"name": "Normale Samen",
|
"name": "Normale Samen",
|
||||||
@@ -26,5 +26,7 @@
|
|||||||
"slice": "Seed3",
|
"slice": "Seed3",
|
||||||
"growth_stages": 6
|
"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
|
libxkbcommon
|
||||||
# linker
|
# linker
|
||||||
lld
|
lld
|
||||||
|
|
||||||
|
openssl
|
||||||
];
|
];
|
||||||
runtimeLibs = pkgs.lib.makeLibraryPath bevyDeps;
|
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::fs::File;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
|
|
||||||
|
/// Global configuration loaded from file, containing balancing numbers and paths.
|
||||||
#[derive(Resource, Deserialize, Debug)]
|
#[derive(Resource, Deserialize, Debug)]
|
||||||
pub struct GameConfig {
|
pub struct GameConfig {
|
||||||
pub grid_width: u32,
|
pub grid_width: u32,
|
||||||
@@ -10,8 +11,11 @@ pub struct GameConfig {
|
|||||||
pub shovel_base_price: u32,
|
pub shovel_base_price: u32,
|
||||||
pub shovel_rate: f32,
|
pub shovel_rate: f32,
|
||||||
pub berry_seeds: Vec<BerrySeedConfig>,
|
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)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct BerrySeedConfig {
|
pub struct BerrySeedConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -52,15 +56,19 @@ impl Default for GameConfig {
|
|||||||
growth_stages: 6,
|
growth_stages: 6,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
wonder_event_url: "wss://pomomon.farm/ws".into(),
|
||||||
|
berries_per_focus_minute: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameConfig {
|
impl GameConfig {
|
||||||
|
/// Reads `config.json` from assets.
|
||||||
pub fn read_config() -> Option<Self> {
|
pub fn read_config() -> Option<Self> {
|
||||||
Self::read_from_path(std::path::Path::new("assets/config.json"))
|
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> {
|
pub fn read_from_path(path: &std::path::Path) -> Option<Self> {
|
||||||
let file = File::open(path).ok()?;
|
let file = File::open(path).ok()?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::prelude::*;
|
|||||||
|
|
||||||
pub mod states;
|
pub mod states;
|
||||||
|
|
||||||
|
/// Handles core engine setup like camera and initial state.
|
||||||
pub struct CorePlugin;
|
pub struct CorePlugin;
|
||||||
|
|
||||||
impl Plugin for CorePlugin {
|
impl Plugin for CorePlugin {
|
||||||
@@ -11,6 +12,7 @@ impl Plugin for CorePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns the main 2D camera.
|
||||||
fn setup_camera(mut commands: Commands) {
|
fn setup_camera(mut commands: Commands) {
|
||||||
commands.spawn(Camera2d::default());
|
commands.spawn(Camera2d::default());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Global states of the application.
|
||||||
#[derive(States, Clone, PartialEq, Eq, Debug, Hash, Default)]
|
#[derive(States, Clone, PartialEq, Eq, Debug, Hash, Default)]
|
||||||
pub enum AppState {
|
pub enum AppState {
|
||||||
#[default]
|
#[default]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Plugin for the main game screen, managing the game loop and environment.
|
||||||
pub struct GameScreenPlugin;
|
pub struct GameScreenPlugin;
|
||||||
|
|
||||||
impl Plugin for 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>) {
|
fn setup(mut clear_color: ResMut<ClearColor>) {
|
||||||
*clear_color = ClearColor(Color::srgb(0.294, 0.412, 0.184));
|
*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>) {
|
fn cleanup(mut clear_color: ResMut<ClearColor>) {
|
||||||
*clear_color = ClearColor(Color::srgb(0.2, 0.2, 0.2));
|
*clear_color = ClearColor(Color::srgb(0.2, 0.2, 0.2));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
use super::errors::GridError;
|
use super::errors::GridError;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Component representing a single tile on the grid.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Tile {
|
pub struct Tile {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
pub y: u32,
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visual marker component for the crop on a tile.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct CropVisual;
|
pub struct CropVisual;
|
||||||
|
|
||||||
|
/// Visual marker component for the water on a tile.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct WaterVisual;
|
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 {
|
pub enum TileState {
|
||||||
#[default]
|
#[default]
|
||||||
Unclaimed,
|
Unclaimed,
|
||||||
@@ -38,6 +42,7 @@ impl TileState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resource containing grid dimensions and tile entities.
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
@@ -46,6 +51,7 @@ pub struct Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
|
/// Returns the entity of the tile at the given position.
|
||||||
pub fn get_tile(&self, pos: (u32, u32)) -> Result<Entity, GridError> {
|
pub fn get_tile(&self, pos: (u32, u32)) -> Result<Entity, GridError> {
|
||||||
if pos.0 >= self.width || pos.1 >= self.height {
|
if pos.0 >= self.width || pos.1 >= self.height {
|
||||||
return Err(GridError::OutOfBounds {
|
return Err(GridError::OutOfBounds {
|
||||||
@@ -56,6 +62,7 @@ impl Grid {
|
|||||||
Ok(self.tiles[pos.0 as usize][pos.1 as usize])
|
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>(
|
pub fn map_tile_state<F>(
|
||||||
&self,
|
&self,
|
||||||
pos: (u32, u32),
|
pos: (u32, u32),
|
||||||
@@ -74,4 +81,19 @@ impl Grid {
|
|||||||
*tile_state = mapper(&*tile_state);
|
*tile_state = mapper(&*tile_state);
|
||||||
Ok(())
|
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;
|
pub const TILE_SIZE: f32 = 32.0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{error::Error, fmt};
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
/// Errors related to grid operations.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum GridError {
|
pub enum GridError {
|
||||||
OutOfBounds { x: i32, y: i32 },
|
OutOfBounds { x: i32, y: i32 },
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use components::{CropVisual, WaterVisual};
|
use components::{CropVisual, WaterVisual};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
/// Manages the game grid, including tiles, visuals, and updates.
|
||||||
pub struct GridPlugin;
|
pub struct GridPlugin;
|
||||||
|
|
||||||
impl Plugin for 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>) {
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
||||||
let grid_width = config.grid_width;
|
let grid_width = config.grid_width;
|
||||||
let grid_height = config.grid_height;
|
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);
|
let mut column = Vec::with_capacity(grid_height as usize);
|
||||||
|
|
||||||
for y in 0..grid_height {
|
for y in 0..grid_height {
|
||||||
|
let initial_state = if x == 1 && y == 1 {
|
||||||
|
TileState::Empty
|
||||||
|
} else {
|
||||||
|
TileState::Unclaimed
|
||||||
|
};
|
||||||
|
|
||||||
let tile_entity = commands
|
let tile_entity = commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Tile { x, y },
|
Tile { x, y },
|
||||||
TileState::Unclaimed,
|
initial_state.clone(),
|
||||||
AseSlice {
|
AseSlice {
|
||||||
name: "Unclaimed".into(),
|
name: match initial_state {
|
||||||
aseprite: asset_server.load("tiles/tile-unclaimed.aseprite"),
|
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(),
|
Sprite::default(),
|
||||||
Transform::from_translation(grid_to_world_coords(
|
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>>) {
|
fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
||||||
for tile_entity in tile_query.iter() {
|
for tile_entity in tile_query.iter() {
|
||||||
commands.entity(tile_entity).despawn();
|
commands.entity(tile_entity).despawn();
|
||||||
@@ -87,8 +106,12 @@ fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
|||||||
commands.remove_resource::<Grid>();
|
commands.remove_resource::<Grid>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates tile visuals based on their state (e.g., crop growth, highlighting).
|
||||||
fn update_tiles(
|
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 crop_query: Query<
|
||||||
(&mut Visibility, &mut Transform, &mut AseSlice),
|
(&mut Visibility, &mut Transform, &mut AseSlice),
|
||||||
(With<CropVisual>, Without<WaterVisual>, Without<Tile>),
|
(With<CropVisual>, Without<WaterVisual>, Without<Tile>),
|
||||||
@@ -99,8 +122,27 @@ fn update_tiles(
|
|||||||
>,
|
>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
game_config: Res<GameConfig>,
|
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 {
|
slice.name = match state {
|
||||||
TileState::Unclaimed => "Unclaimed",
|
TileState::Unclaimed => "Unclaimed",
|
||||||
TileState::Empty => "Empty",
|
TileState::Empty => "Empty",
|
||||||
@@ -133,6 +175,34 @@ fn update_tiles(
|
|||||||
_ => Vec3::ONE,
|
_ => 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() {
|
for child in children.iter() {
|
||||||
if let Ok((mut visibility, mut transform, mut sprite)) = crop_query.get_mut(child) {
|
if let Ok((mut visibility, mut transform, mut sprite)) = crop_query.get_mut(child) {
|
||||||
*visibility = match state {
|
*visibility = match state {
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use super::errors::GridError;
|
use super::errors::GridError;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Calculates the starting X coordinate for centering the grid.
|
||||||
pub fn grid_start_x(grid_width: u32) -> f32 {
|
pub fn grid_start_x(grid_width: u32) -> f32 {
|
||||||
-(grid_width as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
-(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 {
|
pub fn grid_start_y(grid_height: u32) -> f32 {
|
||||||
-(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
-(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts world coordinates to grid coordinates.
|
||||||
pub fn world_to_grid_coords(
|
pub fn world_to_grid_coords(
|
||||||
world_pos: Vec3,
|
world_pos: Vec3,
|
||||||
grid_width: u32,
|
grid_width: u32,
|
||||||
@@ -30,6 +33,7 @@ pub fn world_to_grid_coords(
|
|||||||
Ok((x as u32, y as u32))
|
Ok((x as u32, y as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts grid coordinates to world coordinates.
|
||||||
pub fn grid_to_world_coords(
|
pub fn grid_to_world_coords(
|
||||||
grid_x: u32,
|
grid_x: u32,
|
||||||
grid_y: u32,
|
grid_y: u32,
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
use crate::{features::phase::components::TimerSettings, prelude::*};
|
use crate::{features::phase::components::TimerSettings, prelude::*};
|
||||||
|
|
||||||
|
/// Markers for root UI nodes.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
Status,
|
Status,
|
||||||
Settings,
|
Settings,
|
||||||
|
ShovelOverlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Markers for text components in the HUD.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum TextType {
|
pub enum TextType {
|
||||||
Phase,
|
Phase,
|
||||||
Timer,
|
Timer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Markers for buttons in the HUD and settings.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
SettingsOpen,
|
SettingsOpen,
|
||||||
SettingsClose,
|
|
||||||
SettingsExit,
|
SettingsExit,
|
||||||
SettingsSave,
|
SettingsSave,
|
||||||
|
SettingsAchievements,
|
||||||
SettingsTimerChange {
|
SettingsTimerChange {
|
||||||
input: SettingsTimerInput,
|
input: SettingsTimerInput,
|
||||||
amount: i32,
|
amount: i32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Types of timers available in the game.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum TimerType {
|
pub enum TimerType {
|
||||||
Focus,
|
Focus,
|
||||||
@@ -32,6 +37,7 @@ pub enum TimerType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TimerSettings {
|
impl TimerSettings {
|
||||||
|
/// Changes the duration of a specific timer.
|
||||||
pub fn change(&mut self, timer_type: &TimerType, amount: i32) {
|
pub fn change(&mut self, timer_type: &TimerType, amount: i32) {
|
||||||
match timer_type {
|
match timer_type {
|
||||||
TimerType::Focus => {
|
TimerType::Focus => {
|
||||||
@@ -59,6 +65,7 @@ impl TimerSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Input types for adjusting timer settings.
|
||||||
#[derive(Component, Clone)]
|
#[derive(Component, Clone)]
|
||||||
pub enum SettingsTimerInput {
|
pub enum SettingsTimerInput {
|
||||||
Minutes(TimerType),
|
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::phase::components::TimerSettings;
|
||||||
use crate::features::savegame::messages::SavegameDumpMessage;
|
use crate::features::savegame::messages::SavegameDumpMessage;
|
||||||
use crate::features::{inventory, shop};
|
use crate::features::{inventory, shop};
|
||||||
@@ -8,6 +9,7 @@ use ui::*;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Plugin for the Head-Up Display (HUD) containing status bars and buttons.
|
||||||
pub struct HudPlugin;
|
pub struct HudPlugin;
|
||||||
|
|
||||||
impl Plugin for HudPlugin {
|
impl Plugin for HudPlugin {
|
||||||
@@ -16,12 +18,19 @@ impl Plugin for HudPlugin {
|
|||||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
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((
|
commands.spawn((
|
||||||
RootMarker::Status,
|
RootMarker::Status,
|
||||||
Node {
|
Node {
|
||||||
@@ -44,34 +53,83 @@ fn setup(mut commands: Commands) {
|
|||||||
button(
|
button(
|
||||||
shop::components::ButtonType::ShopOpen,
|
shop::components::ButtonType::ShopOpen,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
padding: UiRect::all(px(10)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|color| text("Shop [P]", 16.0, color)
|
|color| text("Shop [P]", 16.0, color)
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
inventory::components::ButtonType::InventoryOpen,
|
inventory::components::ButtonType::InventoryOpen,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
padding: UiRect::all(px(10)),
|
|color| text("Inventar [I]", 16.0, color)
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|color| text("Inventar", 16.0, color)
|
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
ButtonType::SettingsOpen,
|
ButtonType::SettingsOpen,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
padding: UiRect::all(px(10)),
|
|color| text("Einstellungen [Esc]", 16.0, color)
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|color| text("Einstellungen", 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)>) {
|
fn update_status(phase_res: Res<CurrentPhase>, mut text_query: Query<(&mut Text, &TextType)>) {
|
||||||
if !phase_res.is_changed() {
|
if !phase_res.is_changed() {
|
||||||
return;
|
return;
|
||||||
@@ -86,28 +144,29 @@ fn update_status(phase_res: Res<CurrentPhase>, mut text_query: Query<(&mut Text,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles HUD button interactions.
|
||||||
fn buttons(
|
fn buttons(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
root_query: Query<(Entity, &RootMarker)>,
|
|
||||||
mut savegame_messages: MessageWriter<SavegameDumpMessage>,
|
mut savegame_messages: MessageWriter<SavegameDumpMessage>,
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
mut timer_settings: ResMut<TimerSettings>,
|
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 {
|
for (interaction, button_type) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => match button_type {
|
Interaction::Pressed => match button_type {
|
||||||
ButtonType::SettingsOpen => {
|
ButtonType::SettingsOpen => {
|
||||||
open_settings(&mut commands);
|
open_settings(&mut commands);
|
||||||
}
|
}
|
||||||
ButtonType::SettingsClose => {
|
|
||||||
for (entity, root) in root_query.iter() {
|
|
||||||
match *root {
|
|
||||||
RootMarker::Settings => commands.entity(entity).despawn(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ButtonType::SettingsExit => {
|
ButtonType::SettingsExit => {
|
||||||
savegame_messages.write(SavegameDumpMessage);
|
savegame_messages.write(SavegameDumpMessage);
|
||||||
next_state.set(AppState::StartScreen);
|
next_state.set(AppState::StartScreen);
|
||||||
@@ -115,12 +174,20 @@ fn buttons(
|
|||||||
ButtonType::SettingsSave => {
|
ButtonType::SettingsSave => {
|
||||||
savegame_messages.write(SavegameDumpMessage);
|
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 {
|
ButtonType::SettingsTimerChange { input, amount } => match input {
|
||||||
SettingsTimerInput::Minutes(timer_type) => {
|
SettingsTimerInput::Minutes(timer_type) => {
|
||||||
timer_settings.change(timer_type, 60 * amount)
|
timer_settings.change(timer_type, 60 * amount * shift_multiplier)
|
||||||
}
|
}
|
||||||
SettingsTimerInput::Seconds(timer_type) => {
|
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>>) {
|
fn cleanup(mut commands: Commands, query: Query<Entity, With<RootMarker>>) {
|
||||||
for entity in query.iter() {
|
for entity in query.iter() {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the timer settings display in the settings menu.
|
||||||
fn update_timer_settings(
|
fn update_timer_settings(
|
||||||
timer_settings: ResMut<TimerSettings>,
|
timer_settings: ResMut<TimerSettings>,
|
||||||
mut query: Query<(&SettingsTimerInput, &mut Text)>,
|
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 super::timer_settings::timer_settings;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Spawns the settings popup.
|
||||||
pub fn open_settings(commands: &mut Commands) {
|
pub fn open_settings(commands: &mut Commands) {
|
||||||
commands
|
spawn_popup(
|
||||||
.spawn((
|
commands,
|
||||||
RootMarker::Settings,
|
RootMarker::Settings,
|
||||||
Node {
|
"Spiel Einstellungen",
|
||||||
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 {
|
Node {
|
||||||
width: px(700),
|
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()
|
..default()
|
||||||
},
|
},
|
||||||
|color| text("X", 24.0, color)
|
|parent| {
|
||||||
),
|
parent.spawn((
|
||||||
],
|
Node::vstack(px(10)),
|
||||||
));
|
children![
|
||||||
|
button(
|
||||||
parent
|
|
||||||
.spawn(Node::vstack(px(10)))
|
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn(button(
|
|
||||||
ButtonType::SettingsExit,
|
ButtonType::SettingsExit,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
padding: UiRect::all(px(10)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|color| text("Spiel verlassen", 24.0, color)
|
|color| text("Spiel verlassen", 24.0, color)
|
||||||
));
|
),
|
||||||
|
button(
|
||||||
parent.spawn(button(
|
|
||||||
ButtonType::SettingsSave,
|
ButtonType::SettingsSave,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
padding: UiRect::all(px(10)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|color| text("Spiel speichern", 24.0, color)
|
|color| text("Spiel speichern", 24.0, color)
|
||||||
));
|
),
|
||||||
|
button(
|
||||||
parent.spawn((
|
ButtonType::SettingsAchievements,
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
Node::from_padding(UiRect::all(px(10))),
|
||||||
|
|color| text("Erfolge", 24.0, color)
|
||||||
|
),(
|
||||||
Node {
|
Node {
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
..Node::hstack(px(30))
|
..Node::hstack(px(30))
|
||||||
@@ -122,8 +84,9 @@ pub fn open_settings(commands: &mut Commands) {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
));
|
));
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::super::components::*;
|
use super::super::components::*;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Creates a UI bundle for a specific timer setting.
|
||||||
pub fn timer_settings(timer_type: TimerType) -> impl Bundle {
|
pub fn timer_settings(timer_type: TimerType) -> impl Bundle {
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use crate::features::{
|
use crate::features::{
|
||||||
|
hud::ui::settings::open_settings,
|
||||||
input::utils::mouse_to_grid,
|
input::utils::mouse_to_grid,
|
||||||
|
inventory::{components::ItemStack, ui::open_inventory},
|
||||||
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
|
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
|
||||||
pom::messages::InvalidMoveMessage,
|
pom::messages::InvalidMoveMessage,
|
||||||
shop::ui::open_shop,
|
shop::ui::open_shop,
|
||||||
|
ui::{messages::ClosePopupMessage, ui::popups::PopupRoot},
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use bevy::input::mouse::MouseButton;
|
use bevy::input::mouse::MouseButton;
|
||||||
@@ -10,6 +13,7 @@ use bevy::window::PrimaryWindow;
|
|||||||
|
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
/// Handles user input for the game.
|
||||||
pub struct InputPlugin;
|
pub struct InputPlugin;
|
||||||
|
|
||||||
impl Plugin for 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, next_phase.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
app.add_systems(Update, shop_keybind.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(
|
fn move_click(
|
||||||
mut move_messages: MessageWriter<MoveMessage>,
|
mut move_messages: MessageWriter<MoveMessage>,
|
||||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
@@ -49,7 +61,13 @@ fn move_click(
|
|||||||
config: Res<GameConfig>,
|
config: Res<GameConfig>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
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 {
|
match phase.0 {
|
||||||
Phase::Focus { .. } => return,
|
Phase::Focus { .. } => return,
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -65,6 +83,7 @@ fn move_click(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles left-click interactions (tile selection, shovel).
|
||||||
fn interact_click(
|
fn interact_click(
|
||||||
mut tile_click_messages: MessageWriter<TileClickMessage>,
|
mut tile_click_messages: MessageWriter<TileClickMessage>,
|
||||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
@@ -74,12 +93,25 @@ fn interact_click(
|
|||||||
config: Res<GameConfig>,
|
config: Res<GameConfig>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
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 {
|
match phase.0 {
|
||||||
Phase::Focus { .. } => return,
|
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 mouse_btn.just_pressed(MouseButton::Left) {
|
||||||
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
||||||
return;
|
return;
|
||||||
@@ -88,10 +120,71 @@ fn interact_click(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
tile_click_messages.write(TileClickMessage { x, y });
|
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(
|
fn debug_click(
|
||||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
@@ -138,6 +231,7 @@ fn debug_click(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pauses/resumes the phase timer on Space press.
|
||||||
fn phase_timer_pause(
|
fn phase_timer_pause(
|
||||||
mut pause_messages: MessageWriter<PhaseTimerPauseMessage>,
|
mut pause_messages: MessageWriter<PhaseTimerPauseMessage>,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
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>>) {
|
fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInput<KeyCode>>) {
|
||||||
if keys.just_pressed(KeyCode::Enter) {
|
if keys.just_pressed(KeyCode::Enter) {
|
||||||
messages.write(NextPhaseMessage);
|
messages.write(NextPhaseMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens the shop on 'P' press.
|
||||||
fn shop_keybind(
|
fn shop_keybind(
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
game_config: Res<GameConfig>,
|
game_config: Res<GameConfig>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
) {
|
) {
|
||||||
if keys.just_pressed(KeyCode::KeyP) {
|
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 crate::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
/// Converts mouse position to grid coordinates, respecting UI blocks.
|
||||||
pub fn mouse_to_grid(
|
pub fn mouse_to_grid(
|
||||||
window: Single<&Window, With<PrimaryWindow>>,
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::features::config::components::BerrySeedConfig;
|
use crate::features::config::components::BerrySeedConfig;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Types of items available in the game.
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum ItemType {
|
pub enum ItemType {
|
||||||
Berry,
|
Berry,
|
||||||
@@ -80,8 +81,8 @@ impl ItemType {
|
|||||||
aseprite: asset_server.load("berry.aseprite"),
|
aseprite: asset_server.load("berry.aseprite"),
|
||||||
},
|
},
|
||||||
ItemType::Shovel => AseSlice {
|
ItemType::Shovel => AseSlice {
|
||||||
name: "Berry".into(),
|
name: "Shovel".into(),
|
||||||
aseprite: asset_server.load("berry.aseprite"),
|
aseprite: asset_server.load("shovel.aseprite"),
|
||||||
},
|
},
|
||||||
ItemType::BerrySeed { name } => {
|
ItemType::BerrySeed { name } => {
|
||||||
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *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 struct ItemStack {
|
||||||
pub item_type: ItemType,
|
pub item_type: ItemType,
|
||||||
pub amount: u32,
|
pub amount: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resource containing all items owned by the player.
|
||||||
#[derive(Resource, Default, Serialize, Deserialize)]
|
#[derive(Resource, Default, Serialize, Deserialize)]
|
||||||
pub struct Inventory {
|
pub struct Inventory {
|
||||||
pub items: Vec<Entity>,
|
pub items: Vec<Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Inventory {
|
impl Inventory {
|
||||||
pub fn has_item(&self, items_query: Query<&ItemStack>) -> bool {
|
/// Checks if the inventory contains a specific item type.
|
||||||
self.items
|
pub fn has_item_type(&self, items_query: &Query<&ItemStack>, item_type: ItemType) -> bool {
|
||||||
.iter()
|
self.items.iter().any(|&entity| {
|
||||||
.map(|entity| items_query.get(*entity).ok())
|
if let Ok(stack) = items_query.get(entity) {
|
||||||
.find(|option| option.is_some())
|
stack.item_type == item_type
|
||||||
.is_some()
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds or removes items from the inventory.
|
||||||
pub fn update_item_stack(
|
pub fn update_item_stack(
|
||||||
&mut self,
|
&mut self,
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
@@ -191,13 +198,14 @@ impl Inventory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Markers for inventory UI root nodes.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
Inventory,
|
Inventory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Markers for inventory-related buttons.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
InventoryOpen,
|
InventoryOpen,
|
||||||
InventoryClose,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use crate::features::phase::components::SessionTracker;
|
||||||
use crate::{features::inventory::ui::open_inventory, prelude::*};
|
use crate::{features::inventory::ui::open_inventory, prelude::*};
|
||||||
use components::*;
|
use components::*;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Plugin for the inventory system, including storage and UI.
|
||||||
pub struct InventoryPlugin;
|
pub struct InventoryPlugin;
|
||||||
|
|
||||||
impl Plugin for InventoryPlugin {
|
impl Plugin for InventoryPlugin {
|
||||||
@@ -17,11 +20,11 @@ impl Plugin for InventoryPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles inventory button interactions.
|
||||||
fn buttons(
|
fn buttons(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
itemstack_query: Query<&ItemStack>,
|
itemstack_query: Query<&ItemStack>,
|
||||||
root_query: Query<(Entity, &RootMarker)>,
|
|
||||||
game_config: Res<GameConfig>,
|
game_config: Res<GameConfig>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
@@ -31,30 +34,27 @@ fn buttons(
|
|||||||
ButtonType::InventoryOpen => {
|
ButtonType::InventoryOpen => {
|
||||||
open_inventory(&mut commands, itemstack_query, &game_config, &asset_server);
|
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)]
|
#[cfg(debug_assertions)]
|
||||||
fn debug_modify_berries(
|
fn debug_modify_berries(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut items: Query<&mut ItemStack>,
|
mut items: Query<&mut ItemStack>,
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>,
|
||||||
) {
|
) {
|
||||||
if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
||||||
if keys.just_pressed(KeyCode::ArrowUp) {
|
if keys.just_pressed(KeyCode::ArrowUp) {
|
||||||
println!("Adding 1 berry using debug bind");
|
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) {
|
} else if keys.just_pressed(KeyCode::ArrowDown) {
|
||||||
println!("Removing 1 berry using debug bind");
|
println!("Removing 1 berry using debug bind");
|
||||||
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, -1);
|
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::prelude::GameConfig;
|
||||||
use crate::{features::inventory::ui::list_itemstack, prelude::*};
|
use crate::{features::inventory::ui::list_itemstack, prelude::*};
|
||||||
|
|
||||||
|
/// Spawns the inventory popup.
|
||||||
pub fn open_inventory(
|
pub fn open_inventory(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
items: Query<&ItemStack>,
|
items: Query<&ItemStack>,
|
||||||
game_config: &Res<GameConfig>,
|
game_config: &Res<GameConfig>,
|
||||||
asset_server: &Res<AssetServer>,
|
asset_server: &Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
commands
|
spawn_popup(
|
||||||
.spawn((
|
commands,
|
||||||
RootMarker::Inventory,
|
RootMarker::Inventory,
|
||||||
Node {
|
"Inventar",
|
||||||
position_type: PositionType::Absolute,
|
Node::default(),
|
||||||
width: percent(100),
|
|parent| {
|
||||||
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)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
parent
|
parent
|
||||||
.spawn(Node {
|
.spawn(Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
@@ -61,10 +22,13 @@ pub fn open_inventory(
|
|||||||
..Node::vstack(px(10))
|
..Node::vstack(px(10))
|
||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
for itemstack in items.iter() {
|
items
|
||||||
parent.spawn(list_itemstack(itemstack, game_config, asset_server));
|
.iter()
|
||||||
}
|
.map(|item| list_itemstack(item, game_config, asset_server))
|
||||||
});
|
.for_each(|itemstack| {
|
||||||
});
|
parent.spawn(itemstack);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Creates a UI bundle for a single item stack in the inventory list.
|
||||||
pub fn list_itemstack(
|
pub fn list_itemstack(
|
||||||
itemstack: &ItemStack,
|
itemstack: &ItemStack,
|
||||||
game_config: &GameConfig,
|
game_config: &GameConfig,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod achievement;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod core;
|
pub mod core;
|
||||||
pub mod game_screen;
|
pub mod game_screen;
|
||||||
@@ -5,22 +6,27 @@ pub mod grid;
|
|||||||
pub mod hud;
|
pub mod hud;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod inventory;
|
pub mod inventory;
|
||||||
|
pub mod notification;
|
||||||
pub mod phase;
|
pub mod phase;
|
||||||
pub mod pom;
|
pub mod pom;
|
||||||
pub mod savegame;
|
pub mod savegame;
|
||||||
pub mod shop;
|
pub mod shop;
|
||||||
pub mod start_screen;
|
pub mod start_screen;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod wonderevent;
|
||||||
|
|
||||||
|
pub use achievement::AchievementPlugin;
|
||||||
pub use core::CorePlugin;
|
pub use core::CorePlugin;
|
||||||
pub use game_screen::GameScreenPlugin;
|
pub use game_screen::GameScreenPlugin;
|
||||||
pub use grid::GridPlugin;
|
pub use grid::GridPlugin;
|
||||||
pub use hud::HudPlugin;
|
pub use hud::HudPlugin;
|
||||||
pub use input::InputPlugin;
|
pub use input::InputPlugin;
|
||||||
pub use inventory::InventoryPlugin;
|
pub use inventory::InventoryPlugin;
|
||||||
|
pub use notification::NotificationPlugin;
|
||||||
pub use phase::PhasePlugin;
|
pub use phase::PhasePlugin;
|
||||||
pub use pom::PomPlugin;
|
pub use pom::PomPlugin;
|
||||||
pub use savegame::SavegamePlugin;
|
pub use savegame::SavegamePlugin;
|
||||||
pub use shop::ShopPlugin;
|
pub use shop::ShopPlugin;
|
||||||
pub use start_screen::StartScreenPlugin;
|
pub use start_screen::StartScreenPlugin;
|
||||||
pub use ui::UiPlugin;
|
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 super::utils::format_time;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Represents the different states of the Pomodoro timer.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Phase {
|
pub enum Phase {
|
||||||
Break { duration: f32 },
|
Break { duration: f32 },
|
||||||
@@ -37,9 +38,11 @@ impl Phase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resource holding the current phase state.
|
||||||
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct CurrentPhase(pub Phase);
|
pub struct CurrentPhase(pub Phase);
|
||||||
|
|
||||||
|
/// Configuration for phase durations.
|
||||||
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
#[derive(Resource, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct TimerSettings {
|
pub struct TimerSettings {
|
||||||
pub focus_duration: u32,
|
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)]
|
#[derive(Resource, Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct SessionTracker {
|
pub struct SessionTracker {
|
||||||
pub completed_focus_phases: u32,
|
pub completed_focus_phases: u32,
|
||||||
|
pub total_berries_earned: u32,
|
||||||
|
pub total_plants_withered: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Message sent when a phase timer reaches zero.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct PhaseTimerFinishedMessage {
|
pub struct PhaseTimerFinishedMessage {
|
||||||
pub phase: Phase,
|
pub phase: Phase,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Message to toggle pause state.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct PhaseTimerPauseMessage;
|
pub struct PhaseTimerPauseMessage;
|
||||||
|
|
||||||
|
/// Message to proceed to the next phase.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct NextPhaseMessage;
|
pub struct NextPhaseMessage;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod components;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
/// Plugin managing the Pomodoro phase timer and state.
|
||||||
pub struct PhasePlugin;
|
pub struct PhasePlugin;
|
||||||
|
|
||||||
impl Plugin for PhasePlugin {
|
impl Plugin for PhasePlugin {
|
||||||
@@ -22,7 +23,13 @@ impl Plugin for PhasePlugin {
|
|||||||
app.add_systems(OnEnter(AppState::GameScreen), load_rules);
|
app.add_systems(OnEnter(AppState::GameScreen), load_rules);
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
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)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -30,6 +37,7 @@ impl Plugin for PhasePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Debug system to shorten phase duration for testing.
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
fn debug_short_phase_duration(
|
fn debug_short_phase_duration(
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
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>) {
|
fn load_rules(mut phase_res: ResMut<CurrentPhase>, settings: Res<TimerSettings>) {
|
||||||
let phase = &mut phase_res.0;
|
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(
|
fn tick_timer(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
mut phase_res: ResMut<CurrentPhase>,
|
||||||
mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>,
|
mut finish_writer: MessageWriter<PhaseTimerFinishedMessage>,
|
||||||
@@ -87,7 +99,11 @@ fn tick_timer(
|
|||||||
completed_phase: Box::new(completed),
|
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);
|
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(
|
fn handle_pause(
|
||||||
mut messages: MessageReader<PhaseTimerPauseMessage>,
|
mut messages: MessageReader<PhaseTimerPauseMessage>,
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
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(
|
pub fn next_phase(
|
||||||
current_phase: &mut CurrentPhase,
|
current_phase: &mut CurrentPhase,
|
||||||
session_tracker: &mut SessionTracker,
|
session_tracker: &SessionTracker,
|
||||||
settings: &TimerSettings,
|
settings: &TimerSettings,
|
||||||
) {
|
) {
|
||||||
if let Phase::Finished { completed_phase } = ¤t_phase.0 {
|
if let Phase::Finished { completed_phase } = ¤t_phase.0 {
|
||||||
match **completed_phase {
|
match **completed_phase {
|
||||||
Phase::Focus { .. } => {
|
Phase::Focus { .. } => {
|
||||||
session_tracker.completed_focus_phases += 1;
|
|
||||||
|
|
||||||
let is_long_break = session_tracker.completed_focus_phases > 0
|
let is_long_break = session_tracker.completed_focus_phases > 0
|
||||||
&& session_tracker.completed_focus_phases % settings.long_break_interval == 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(
|
pub fn handle_continue(
|
||||||
mut messages: MessageReader<NextPhaseMessage>,
|
mut messages: MessageReader<NextPhaseMessage>,
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
mut phase_res: ResMut<CurrentPhase>,
|
||||||
@@ -167,7 +225,7 @@ pub fn handle_continue(
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
next_phase(&mut phase_res, &mut session_tracker, &settings);
|
next_phase(&mut phase_res, &session_tracker, &settings);
|
||||||
|
|
||||||
if entering_break {
|
if entering_break {
|
||||||
println!("Growing crops and resetting watered state.");
|
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 {
|
*state = TileState::Occupied {
|
||||||
seed: seed.clone(),
|
seed: seed.clone(),
|
||||||
watered: false,
|
watered: false,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// Formats seconds into MM:SS or HH:MM:SS string.
|
||||||
pub fn format_time(seconds: f32) -> String {
|
pub fn format_time(seconds: f32) -> String {
|
||||||
let seconds = seconds.max(0.0) as u32;
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum InteractionAction {
|
pub enum InteractionAction {
|
||||||
Plant(ItemType),
|
Plant(ItemType),
|
||||||
@@ -88,6 +89,7 @@ impl InteractionAction {
|
|||||||
item_stack_query: &mut Query<&mut ItemStack>,
|
item_stack_query: &mut Query<&mut ItemStack>,
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
game_config: &GameConfig,
|
game_config: &GameConfig,
|
||||||
|
session_tracker: &mut SessionTracker,
|
||||||
) {
|
) {
|
||||||
let Ok(tile_entity) = grid.get_tile(pos) else {
|
let Ok(tile_entity) = grid.get_tile(pos) else {
|
||||||
println!("Error during interaction: Couldn't get tile_entity");
|
println!("Error during interaction: Couldn't get tile_entity");
|
||||||
@@ -165,6 +167,7 @@ impl InteractionAction {
|
|||||||
ItemType::Berry,
|
ItemType::Berry,
|
||||||
config.grants as i32,
|
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 crate::prelude::*;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
/// Marker component for the main character.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Pom;
|
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 struct GridPosition {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
pub y: u32,
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queue of grid positions to visit.
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct PathQueue {
|
pub struct PathQueue {
|
||||||
pub steps: VecDeque<(u32, u32)>,
|
pub steps: VecDeque<(u32, u32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Movement direction state for animation.
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub enum MovingState {
|
pub enum MovingState {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -38,6 +42,7 @@ impl MovingState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Target tile and action for pending interaction.
|
||||||
#[derive(Component, Default)]
|
#[derive(Component, Default)]
|
||||||
pub struct InteractionTarget {
|
pub struct InteractionTarget {
|
||||||
pub target: Option<(u32, u32)>,
|
pub target: Option<(u32, u32)>,
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
use crate::features::pom::actions::InteractionAction;
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Request to move Pom to a specific tile.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct MoveMessage {
|
pub struct MoveMessage {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
pub y: u32,
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notification that a move request was invalid.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct InvalidMoveMessage {
|
pub struct InvalidMoveMessage {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request to start an interaction sequence.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct InteractStartMessage {
|
pub struct InteractStartMessage {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
@@ -19,6 +22,7 @@ pub struct InteractStartMessage {
|
|||||||
pub action: InteractionAction,
|
pub action: InteractionAction,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Notification that a tile was clicked (requesting context menu).
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct TileClickMessage {
|
pub struct TileClickMessage {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod messages;
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
/// Plugin controlling the main character (Pom) behavior.
|
||||||
pub struct PomPlugin;
|
pub struct PomPlugin;
|
||||||
|
|
||||||
impl Plugin for PomPlugin {
|
impl Plugin for PomPlugin {
|
||||||
@@ -34,6 +35,7 @@ impl Plugin for PomPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws the path Pom will follow using gizmos.
|
||||||
fn draw_path(
|
fn draw_path(
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
query: Query<(&Transform, &PathQueue), With<Pom>>,
|
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>) {
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Pom,
|
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>>) {
|
fn cleanup(mut commands: Commands, pom_query: Query<Entity, With<Pom>>) {
|
||||||
for pom_entity in pom_query.iter() {
|
for pom_entity in pom_query.iter() {
|
||||||
commands.entity(pom_entity).despawn();
|
commands.entity(pom_entity).despawn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates path for manual movement requests.
|
||||||
fn handle_move(
|
fn handle_move(
|
||||||
mut move_messages: MessageReader<MoveMessage>,
|
mut move_messages: MessageReader<MoveMessage>,
|
||||||
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
||||||
@@ -127,6 +132,7 @@ fn handle_move(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculates path to interaction target.
|
||||||
fn handle_interact(
|
fn handle_interact(
|
||||||
mut interact_messages: MessageReader<InteractStartMessage>,
|
mut interact_messages: MessageReader<InteractStartMessage>,
|
||||||
mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>,
|
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)>,
|
mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>,
|
||||||
grid: Res<Grid>,
|
grid: Res<Grid>,
|
||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
@@ -192,6 +199,8 @@ fn perform_interaction(
|
|||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
config: Res<GameConfig>,
|
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() {
|
for (pos, mut target_component, path_queue) in pom_query.iter_mut() {
|
||||||
if let Some(target) = target_component.target {
|
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 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!(
|
println!(
|
||||||
"Performing interaction on tile ({}, {})",
|
"Performing interaction on tile ({}, {})",
|
||||||
target.0, target.1
|
target.0, target.1
|
||||||
@@ -215,6 +235,7 @@ fn perform_interaction(
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&config,
|
&config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +246,7 @@ fn perform_interaction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the Pom character along the path.
|
||||||
fn move_pom(
|
fn move_pom(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut query: Query<(
|
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)>) {
|
fn update_pom(asset_server: Res<AssetServer>, mut query: Query<(&MovingState, &mut AseAnimation)>) {
|
||||||
for (moving_state, mut animation) in query.iter_mut() {
|
for (moving_state, mut animation) in query.iter_mut() {
|
||||||
match moving_state {
|
match moving_state {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
use crate::features::pom::actions::InteractionAction;
|
use crate::features::pom::actions::InteractionAction;
|
||||||
|
use crate::features::ui::messages::ClosePopupMessage;
|
||||||
use crate::features::ui::utils::ui_blocks;
|
use crate::features::ui::utils::ui_blocks;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
/// Marker for context menu UI root.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buttons available in the context menu.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
Interact {
|
Interact {
|
||||||
@@ -18,7 +21,8 @@ pub enum ButtonType {
|
|||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_context_menu(
|
/// Spawns the context menu at the clicked tile position.
|
||||||
|
pub fn open_context_menu(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut tile_click_messages: MessageReader<TileClickMessage>,
|
mut tile_click_messages: MessageReader<TileClickMessage>,
|
||||||
root_query: Query<Entity, With<RootMarker>>,
|
root_query: Query<Entity, With<RootMarker>>,
|
||||||
@@ -56,22 +60,11 @@ pub fn spawn_context_menu(
|
|||||||
let options =
|
let options =
|
||||||
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
||||||
|
|
||||||
commands
|
spawn_context_menu(
|
||||||
.spawn((
|
&mut commands,
|
||||||
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)),
|
|
||||||
RootMarker::ContextMenu,
|
RootMarker::ContextMenu,
|
||||||
GlobalTransform::default(),
|
screen_pos,
|
||||||
))
|
|parent| {
|
||||||
.with_children(|parent| {
|
|
||||||
for option in options {
|
for option in options {
|
||||||
parent.spawn(button(
|
parent.spawn(button(
|
||||||
ButtonType::Interact {
|
ButtonType::Interact {
|
||||||
@@ -80,10 +73,7 @@ pub fn spawn_context_menu(
|
|||||||
action: option.clone(),
|
action: option.clone(),
|
||||||
},
|
},
|
||||||
ButtonVariant::Primary,
|
ButtonVariant::Primary,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(5))),
|
||||||
padding: UiRect::all(px(5)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|c| text(option.clone().get_name(&game_config), 20.0, c), // TODO: add sprite
|
|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(
|
parent.spawn(button(
|
||||||
ButtonType::Cancel,
|
ButtonType::Cancel,
|
||||||
ButtonVariant::Destructive,
|
ButtonVariant::Destructive,
|
||||||
Node {
|
Node::from_padding(UiRect::all(px(5))),
|
||||||
padding: UiRect::all(px(5)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
|c| text("Abbrechen", 20.0, c),
|
|c| text("Abbrechen", 20.0, c),
|
||||||
));
|
));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Closes context menu when clicking elsewhere.
|
||||||
pub fn click_outside_context_menu(
|
pub fn click_outside_context_menu(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
@@ -122,6 +111,7 @@ pub fn click_outside_context_menu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles context menu button clicks.
|
||||||
pub fn buttons(
|
pub fn buttons(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut button_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
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;
|
pub mod context_menu;
|
||||||
|
|
||||||
|
/// Plugin for Pom-related UI (context menu).
|
||||||
pub struct PomUiPlugin;
|
pub struct PomUiPlugin;
|
||||||
|
|
||||||
impl Plugin for PomUiPlugin {
|
impl Plugin for PomUiPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
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(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@@ -18,5 +19,9 @@ impl Plugin for PomUiPlugin {
|
|||||||
Update,
|
Update,
|
||||||
context_menu::buttons.run_if(in_state(AppState::GameScreen)),
|
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 crate::prelude::*;
|
||||||
use std::cmp::Ordering;
|
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)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
x: u32,
|
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 {
|
pub fn manhattan_distance(x1: u32, y1: u32, x2: u32, y2: u32) -> u32 {
|
||||||
x1.abs_diff(x2) + y1.abs_diff(y2)
|
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(
|
pub fn find_path(
|
||||||
start: (u32, u32),
|
start: (u32, u32),
|
||||||
end: (u32, u32),
|
end: (u32, u32),
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
|
use crate::features::achievement::components::AchievementProgress;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Resource containing the path to the current save file.
|
||||||
#[derive(Resource, Clone, Debug)]
|
#[derive(Resource, Clone, Debug)]
|
||||||
pub struct SavegamePath(pub PathBuf);
|
pub struct SavegamePath(pub PathBuf);
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Metadata about a savegame.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct SavegameInfo {
|
pub struct SavegameInfo {
|
||||||
pub path: SavegamePath,
|
pub path: SavegamePath,
|
||||||
pub index: u32,
|
pub index: u32,
|
||||||
pub total_berries: u32,
|
pub total_berries: u32,
|
||||||
pub completed_focus: u32,
|
pub completed_focus: u32,
|
||||||
|
pub achievement_progress: AchievementProgress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper for partial JSON deserialization.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PartialSaveData {
|
struct PartialSaveData {
|
||||||
session_tracker: PartialSessionTracker,
|
session_tracker: PartialSessionTracker,
|
||||||
|
achievement_progress: AchievementProgress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper for partial JSON deserialization of session stats.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PartialSessionTracker {
|
struct PartialSessionTracker {
|
||||||
completed_focus_phases: u32,
|
completed_focus_phases: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
total_berries_earned: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SavegamePath {
|
impl SavegamePath {
|
||||||
|
/// Constructs a new path for a specific save index.
|
||||||
pub fn new(index: u32) -> Self {
|
pub fn new(index: u32) -> Self {
|
||||||
let base_path = get_internal_path().unwrap_or_else(|| {
|
let base_path = get_internal_path().unwrap_or_else(|| {
|
||||||
println!(
|
println!(
|
||||||
@@ -39,6 +49,7 @@ impl SavegamePath {
|
|||||||
Self(base_path.join(format!("savegame-{}.json", index)))
|
Self(base_path.join(format!("savegame-{}.json", index)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lists all available savegames.
|
||||||
pub fn list() -> Vec<SavegameInfo> {
|
pub fn list() -> Vec<SavegameInfo> {
|
||||||
let mut savegames = Vec::new();
|
let mut savegames = Vec::new();
|
||||||
|
|
||||||
@@ -77,8 +88,9 @@ impl SavegamePath {
|
|||||||
savegames.push(SavegameInfo {
|
savegames.push(SavegameInfo {
|
||||||
path: SavegamePath(path),
|
path: SavegamePath(path),
|
||||||
index,
|
index,
|
||||||
total_berries: 0, // TODO: add total_berries
|
total_berries: data.session_tracker.total_berries_earned,
|
||||||
completed_focus: data.session_tracker.completed_focus_phases,
|
completed_focus: data.session_tracker.completed_focus_phases,
|
||||||
|
achievement_progress: data.achievement_progress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +98,7 @@ impl SavegamePath {
|
|||||||
savegames
|
savegames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a path for a new savegame (incremented index).
|
||||||
pub fn next() -> Self {
|
pub fn next() -> Self {
|
||||||
let savegames = Self::list();
|
let savegames = Self::list();
|
||||||
let next_index = savegames.last().map(|s| s.index + 1).unwrap_or(0);
|
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)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
PopupSavegameLoad,
|
PopupSavegameLoad,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buttons for savegame management.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
SavegameLoad { savegame_path: SavegamePath },
|
SavegameLoad { savegame_path: SavegamePath },
|
||||||
SavegameDelete { savegame_path: SavegamePath },
|
SavegameDelete { savegame_path: SavegamePath },
|
||||||
PopupClose,
|
Achievements { savegame: SavegameInfo },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Trigger to save the current game.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct SavegameDumpMessage;
|
pub struct SavegameDumpMessage;
|
||||||
|
|
||||||
|
/// Trigger to load a game from disk.
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
pub struct SavegameLoadMessage;
|
pub struct SavegameLoadMessage;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
use crate::features::achievement::components::AchievementProgress;
|
||||||
use crate::features::phase::components::{SessionTracker, TimerSettings};
|
use crate::features::phase::components::{SessionTracker, TimerSettings};
|
||||||
use crate::features::savegame::ui::load_popup_handler;
|
use crate::features::savegame::ui::load_popup_handler;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use components::*;
|
||||||
use messages::*;
|
use messages::*;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
@@ -9,6 +11,7 @@ pub mod components;
|
|||||||
pub mod messages;
|
pub mod messages;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Plugin dealing with savegame loading and saving.
|
||||||
pub struct SavegamePlugin;
|
pub struct SavegamePlugin;
|
||||||
|
|
||||||
impl Plugin for 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, dump_savegame.run_if(in_state(AppState::GameScreen)));
|
||||||
app.add_systems(Update, load_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);
|
app.add_systems(Update, load_popup_handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The structure of a save file.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct SaveData {
|
struct SaveData {
|
||||||
grid_width: u32,
|
grid_width: u32,
|
||||||
@@ -30,11 +35,13 @@ struct SaveData {
|
|||||||
tiles: Vec<Vec<TileState>>,
|
tiles: Vec<Vec<TileState>>,
|
||||||
current_phase: CurrentPhase,
|
current_phase: CurrentPhase,
|
||||||
session_tracker: SessionTracker,
|
session_tracker: SessionTracker,
|
||||||
|
achievement_progress: AchievementProgress,
|
||||||
timer_settings: TimerSettings,
|
timer_settings: TimerSettings,
|
||||||
pom_position: GridPosition,
|
pom_position: GridPosition,
|
||||||
inventory: Vec<ItemStack>,
|
inventory: Vec<ItemStack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serializes game state and writes it to a file.
|
||||||
fn dump_savegame(
|
fn dump_savegame(
|
||||||
mut messages: MessageReader<SavegameDumpMessage>,
|
mut messages: MessageReader<SavegameDumpMessage>,
|
||||||
save_path: Res<SavegamePath>,
|
save_path: Res<SavegamePath>,
|
||||||
@@ -42,6 +49,7 @@ fn dump_savegame(
|
|||||||
tile_query: Query<&TileState>,
|
tile_query: Query<&TileState>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
tracker: Res<SessionTracker>,
|
tracker: Res<SessionTracker>,
|
||||||
|
achievement_progress: Res<AchievementProgress>,
|
||||||
settings: Res<TimerSettings>,
|
settings: Res<TimerSettings>,
|
||||||
pom_query: Query<&GridPosition, With<Pom>>,
|
pom_query: Query<&GridPosition, With<Pom>>,
|
||||||
inventory: Res<Inventory>,
|
inventory: Res<Inventory>,
|
||||||
@@ -80,6 +88,7 @@ fn dump_savegame(
|
|||||||
tiles: tile_states,
|
tiles: tile_states,
|
||||||
current_phase: phase.clone(),
|
current_phase: phase.clone(),
|
||||||
session_tracker: tracker.clone(),
|
session_tracker: tracker.clone(),
|
||||||
|
achievement_progress: achievement_progress.clone(),
|
||||||
timer_settings: settings.clone(),
|
timer_settings: settings.clone(),
|
||||||
pom_position: *pom_pos,
|
pom_position: *pom_pos,
|
||||||
inventory: item_stacks,
|
inventory: item_stacks,
|
||||||
@@ -104,6 +113,7 @@ fn dump_savegame(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads a save file and restores game state.
|
||||||
fn load_savegame(
|
fn load_savegame(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut messages: MessageReader<SavegameLoadMessage>,
|
mut messages: MessageReader<SavegameLoadMessage>,
|
||||||
@@ -112,6 +122,7 @@ fn load_savegame(
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut phase: ResMut<CurrentPhase>,
|
mut phase: ResMut<CurrentPhase>,
|
||||||
mut tracker: ResMut<SessionTracker>,
|
mut tracker: ResMut<SessionTracker>,
|
||||||
|
mut achievement_progress: ResMut<AchievementProgress>,
|
||||||
mut settings: ResMut<TimerSettings>,
|
mut settings: ResMut<TimerSettings>,
|
||||||
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
@@ -128,6 +139,7 @@ fn load_savegame(
|
|||||||
Ok(save_data) => {
|
Ok(save_data) => {
|
||||||
*phase = save_data.current_phase;
|
*phase = save_data.current_phase;
|
||||||
*tracker = save_data.session_tracker;
|
*tracker = save_data.session_tracker;
|
||||||
|
*achievement_progress = save_data.achievement_progress;
|
||||||
*settings = save_data.timer_settings;
|
*settings = save_data.timer_settings;
|
||||||
|
|
||||||
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
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 super::super::components::{ButtonType, RootMarker};
|
||||||
|
use crate::features::achievement::ui::open_achievements_menu;
|
||||||
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
|
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
|
||||||
|
|
||||||
pub fn spawn_load_popup(commands: &mut Commands) {
|
/// Spawns the "Load Game" popup.
|
||||||
commands
|
pub fn spawn_load_popup(commands: &mut Commands, asset_server: &AssetServer) {
|
||||||
.spawn((
|
spawn_popup(
|
||||||
|
commands,
|
||||||
RootMarker::PopupSavegameLoad,
|
RootMarker::PopupSavegameLoad,
|
||||||
|
"Spielstand wählen",
|
||||||
Node {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
width: px(600),
|
||||||
width: percent(100),
|
height: px(500),
|
||||||
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)),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
children![
|
|parent| {
|
||||||
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 {
|
.spawn(Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
flex_direction: FlexDirection::Column,
|
flex_direction: FlexDirection::Column,
|
||||||
overflow: Overflow::scroll_y(),
|
overflow: Overflow::scroll_y(),
|
||||||
margin: UiRect::all(px(20.0)),
|
padding: UiRect::all(px(10)),
|
||||||
row_gap: px(10.0),
|
row_gap: px(10),
|
||||||
..default()
|
..default()
|
||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
for savegame in SavegamePath::list() {
|
for savegame in SavegamePath::list() {
|
||||||
parent.spawn(
|
parent.spawn(button(
|
||||||
button(
|
ButtonType::SavegameLoad {
|
||||||
ButtonType::SavegameLoad { savegame_path: savegame.path.clone() },
|
savegame_path: savegame.path.clone(),
|
||||||
|
},
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..Node::center()
|
..Node::center()
|
||||||
},
|
},
|
||||||
|color| (
|
|color| {
|
||||||
|
(
|
||||||
Node {
|
Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
..Node::hstack(px(10))
|
..Node::hstack(px(10))
|
||||||
},
|
},
|
||||||
children![(
|
children![
|
||||||
|
(
|
||||||
Node {
|
Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
height: 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 {
|
ButtonType::SavegameDelete {
|
||||||
savegame_path: savegame.path.clone()
|
savegame_path: savegame.path.clone()
|
||||||
},
|
},
|
||||||
@@ -114,16 +98,18 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|color| text("X", 24.0, color)
|
|color| text("X", 24.0, color)
|
||||||
)]
|
|
||||||
)
|
)
|
||||||
),
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Handles interactions in the load popup.
|
||||||
pub fn load_popup_handler(
|
pub fn load_popup_handler(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
@@ -135,7 +121,6 @@ pub fn load_popup_handler(
|
|||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => {
|
Interaction::Pressed => {
|
||||||
match button_type {
|
match button_type {
|
||||||
ButtonType::PopupClose => {}
|
|
||||||
ButtonType::SavegameLoad { savegame_path } => {
|
ButtonType::SavegameLoad { savegame_path } => {
|
||||||
commands.insert_resource(savegame_path.clone());
|
commands.insert_resource(savegame_path.clone());
|
||||||
next_state.set(AppState::GameScreen);
|
next_state.set(AppState::GameScreen);
|
||||||
@@ -146,6 +131,9 @@ pub fn load_popup_handler(
|
|||||||
println!("Error while deleting savegame: {:?}", e);
|
println!("Error while deleting savegame: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ButtonType::Achievements { savegame } => {
|
||||||
|
open_achievements_menu(&mut commands, &savegame.achievement_progress);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (entity, root) in root_query.iter() {
|
for (entity, root) in root_query.iter() {
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Markers for shop UI.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
Shop,
|
Shop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buttons in the shop.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
ShopOpen,
|
ShopOpen,
|
||||||
ShopClose,
|
|
||||||
ShopBuyItem(ShopOffer),
|
ShopBuyItem(ShopOffer),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An item available for purchase.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ShopOffer {
|
pub struct ShopOffer {
|
||||||
pub item: ItemStack,
|
pub item: ItemStack,
|
||||||
@@ -19,6 +21,7 @@ pub struct ShopOffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ShopOffer {
|
impl ShopOffer {
|
||||||
|
/// Generates a list of all current offers.
|
||||||
pub fn list_all(game_config: &GameConfig, tile_count: u32) -> Vec<ShopOffer> {
|
pub fn list_all(game_config: &GameConfig, tile_count: u32) -> Vec<ShopOffer> {
|
||||||
let mut offers = Vec::new();
|
let mut offers = Vec::new();
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ impl ShopOffer {
|
|||||||
offers
|
offers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to purchase the offer.
|
||||||
pub fn buy(
|
pub fn buy(
|
||||||
&self,
|
&self,
|
||||||
inventory: &mut Inventory,
|
inventory: &mut Inventory,
|
||||||
@@ -71,4 +75,3 @@ impl ShopOffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use ui::open_shop;
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Plugin for the in-game shop.
|
||||||
pub struct ShopPlugin;
|
pub struct ShopPlugin;
|
||||||
|
|
||||||
impl Plugin for ShopPlugin {
|
impl Plugin for ShopPlugin {
|
||||||
@@ -13,6 +14,7 @@ impl Plugin for ShopPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles shop button interactions.
|
||||||
fn buttons(
|
fn buttons(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
@@ -21,19 +23,14 @@ fn buttons(
|
|||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut items: Query<&mut ItemStack>,
|
mut items: Query<&mut ItemStack>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
) {
|
) {
|
||||||
for (interaction, button_type) in &mut interaction_query {
|
for (interaction, button_type) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => match button_type {
|
Interaction::Pressed => match button_type {
|
||||||
ButtonType::ShopOpen => {
|
ButtonType::ShopOpen => {
|
||||||
open_shop(&mut commands, &game_config, &asset_server);
|
open_shop(&mut commands, &game_config, &asset_server, &grid, &tile_query);
|
||||||
}
|
|
||||||
ButtonType::ShopClose => {
|
|
||||||
for (entity, root) in root_query.iter() {
|
|
||||||
match *root {
|
|
||||||
RootMarker::Shop => commands.entity(entity).despawn(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ButtonType::ShopBuyItem(offer) => {
|
ButtonType::ShopBuyItem(offer) => {
|
||||||
if offer.buy(&mut inventory, &mut commands, &mut items) {
|
if offer.buy(&mut inventory, &mut commands, &mut items) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::super::components::*;
|
use super::super::components::*;
|
||||||
use crate::{features::inventory::ui::item::list_itemstack, prelude::*};
|
use crate::{features::inventory::ui::item::list_itemstack, prelude::*};
|
||||||
|
|
||||||
|
/// Creates the UI bundle for a shop offer.
|
||||||
pub fn shop_offer(
|
pub fn shop_offer(
|
||||||
offer: &ShopOffer,
|
offer: &ShopOffer,
|
||||||
game_config: &GameConfig,
|
game_config: &GameConfig,
|
||||||
@@ -26,6 +27,7 @@ pub fn shop_offer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates the UI bundle for displaying a price.
|
||||||
pub fn shop_price(
|
pub fn shop_price(
|
||||||
price: u32,
|
price: u32,
|
||||||
asset_server: &Res<AssetServer>,
|
asset_server: &Res<AssetServer>,
|
||||||
|
|||||||
@@ -1,64 +1,31 @@
|
|||||||
use super::super::components::*;
|
use super::super::components::*;
|
||||||
use crate::{features::shop::ui::shop_offer, prelude::*};
|
use crate::{features::shop::ui::shop_offer, prelude::*};
|
||||||
|
|
||||||
|
/// Spawns the shop popup.
|
||||||
pub fn open_shop(
|
pub fn open_shop(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
game_config: &GameConfig,
|
game_config: &GameConfig,
|
||||||
asset_server: &Res<AssetServer>,
|
asset_server: &Res<AssetServer>,
|
||||||
|
grid: &Grid,
|
||||||
|
tile_query: &Query<&TileState>,
|
||||||
) {
|
) {
|
||||||
// TODO: calculate tile_count
|
let tile_count = grid.count_claimed_tiles(tile_query);
|
||||||
let offers = ShopOffer::list_all(game_config, 0);
|
let offers = ShopOffer::list_all(game_config, tile_count);
|
||||||
|
|
||||||
commands
|
spawn_popup(
|
||||||
.spawn((
|
commands,
|
||||||
RootMarker::Shop,
|
RootMarker::Shop,
|
||||||
Node {
|
"Einkaufsladen",
|
||||||
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 {
|
Node {
|
||||||
width: px(700),
|
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()
|
..default()
|
||||||
},
|
},
|
||||||
|color| text("X", 24.0, color)
|
|parent| {
|
||||||
),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
|
|
||||||
parent.spawn(Node::vstack(px(10))).with_children(|parent| {
|
parent.spawn(Node::vstack(px(10))).with_children(|parent| {
|
||||||
for offer in offers {
|
for offer in offers {
|
||||||
parent.spawn(shop_offer(&offer, game_config, asset_server));
|
parent.spawn(shop_offer(&offer, game_config, asset_server));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
|
use crate::features::phase::components::TimerSettings;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Markers for main menu UI.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
MainMenu,
|
MainMenu,
|
||||||
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Buttons in the main menu.
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum ButtonType {
|
pub enum ButtonType {
|
||||||
LoadGame,
|
LoadGame,
|
||||||
NewGame,
|
NewGame,
|
||||||
Settings,
|
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::features::savegame::ui::spawn_load_popup;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use components::*;
|
use components::*;
|
||||||
|
use ui::settings::open_settings_menu;
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
/// Plugin for the main menu screen.
|
||||||
pub struct StartScreenPlugin;
|
pub struct StartScreenPlugin;
|
||||||
|
|
||||||
impl Plugin for StartScreenPlugin {
|
impl Plugin for StartScreenPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<StartScreenTimerSettings>();
|
||||||
app.add_systems(OnEnter(AppState::StartScreen), setup);
|
app.add_systems(OnEnter(AppState::StartScreen), setup);
|
||||||
app.add_systems(OnExit(AppState::StartScreen), cleanup);
|
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) {
|
fn setup(mut commands: Commands) {
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
RootMarker::MainMenu,
|
RootMarker::MainMenu,
|
||||||
@@ -60,32 +75,111 @@ fn setup(mut commands: Commands) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles main menu button interactions.
|
||||||
fn menu(
|
fn menu(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
for (interaction, button_type) in &mut interaction_query {
|
for (interaction, button_type) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => {
|
Interaction::Pressed => match button_type {
|
||||||
match button_type {
|
|
||||||
ButtonType::LoadGame => {
|
ButtonType::LoadGame => {
|
||||||
spawn_load_popup(&mut commands);
|
spawn_load_popup(&mut commands, &asset_server);
|
||||||
}
|
}
|
||||||
ButtonType::NewGame => {
|
ButtonType::NewGame => {
|
||||||
commands.insert_resource(SavegamePath::next());
|
commands.insert_resource(SavegamePath::next());
|
||||||
next_state.set(AppState::GameScreen);
|
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>>) {
|
fn cleanup(mut commands: Commands, query: Query<Entity, With<RootMarker>>) {
|
||||||
for entity in query.iter() {
|
for entity in query.iter() {
|
||||||
commands.entity(entity).despawn();
|
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::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Event triggering a scroll action on an entity.
|
||||||
#[derive(EntityEvent, Debug)]
|
#[derive(EntityEvent, Debug)]
|
||||||
#[entity_event(propagate, auto_propagate)]
|
#[entity_event(propagate, auto_propagate)]
|
||||||
pub struct Scroll {
|
pub struct Scroll {
|
||||||
@@ -7,6 +8,7 @@ pub struct Scroll {
|
|||||||
pub delta: Vec2,
|
pub delta: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visual styles for buttons.
|
||||||
#[derive(Component, Clone)]
|
#[derive(Component, Clone)]
|
||||||
pub enum ButtonVariant {
|
pub enum ButtonVariant {
|
||||||
Primary,
|
Primary,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Pixel height of a single scroll line.
|
||||||
pub const LINE_HEIGHT: f32 = 21.0;
|
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);
|
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);
|
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);
|
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};
|
use bevy::{input::mouse::*, picking::hover::HoverMap};
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
|
pub mod messages;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
/// Plugin for general UI behavior like scrolling and button states.
|
||||||
pub struct UiPlugin;
|
pub struct UiPlugin;
|
||||||
|
|
||||||
impl Plugin for UiPlugin {
|
impl Plugin for UiPlugin {
|
||||||
@@ -14,9 +16,12 @@ impl Plugin for UiPlugin {
|
|||||||
app.add_observer(on_scroll_handler);
|
app.add_observer(on_scroll_handler);
|
||||||
|
|
||||||
app.add_systems(Update, update_buttons);
|
app.add_systems(Update, update_buttons);
|
||||||
|
|
||||||
|
app.add_systems(Update, handle_popup_close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads mouse wheel events and triggers scroll actions.
|
||||||
fn scroll_events(
|
fn scroll_events(
|
||||||
mut mouse_wheel_reader: MessageReader<MouseWheel>,
|
mut mouse_wheel_reader: MessageReader<MouseWheel>,
|
||||||
hover_map: Res<HoverMap>,
|
hover_map: Res<HoverMap>,
|
||||||
@@ -42,6 +47,7 @@ fn scroll_events(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates scroll position based on scroll events.
|
||||||
fn on_scroll_handler(
|
fn on_scroll_handler(
|
||||||
mut scroll: On<components::Scroll>,
|
mut scroll: On<components::Scroll>,
|
||||||
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
|
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Creates a standard button UI bundle.
|
||||||
pub fn button<C, R>(
|
pub fn button<C, R>(
|
||||||
button_type: impl Component,
|
button_type: impl Component,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
@@ -24,6 +25,7 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a rounded pill-shaped button UI bundle.
|
||||||
pub fn pill_button<C, R>(
|
pub fn pill_button<C, R>(
|
||||||
button_type: impl Component,
|
button_type: impl Component,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
@@ -48,6 +50,7 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates button colors based on interaction state.
|
||||||
pub fn update_buttons(
|
pub fn update_buttons(
|
||||||
mut interaction_query: Query<
|
mut interaction_query: Query<
|
||||||
(&Interaction, &ButtonVariant, &mut BackgroundColor),
|
(&Interaction, &ButtonVariant, &mut BackgroundColor),
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Trait for easy flexbox layout construction.
|
||||||
pub trait Flexbox {
|
pub trait Flexbox {
|
||||||
fn hstack(spacing: Val) -> Self;
|
fn hstack(spacing: Val) -> Self;
|
||||||
fn vstack(spacing: Val) -> Self;
|
fn vstack(spacing: Val) -> Self;
|
||||||
fn center() -> Self;
|
fn center() -> Self;
|
||||||
|
fn from_padding(padding: UiRect) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flexbox for Node {
|
impl Flexbox for Node {
|
||||||
@@ -30,4 +32,11 @@ impl Flexbox for Node {
|
|||||||
..default()
|
..default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_padding(padding: UiRect) -> Self {
|
||||||
|
Node {
|
||||||
|
padding,
|
||||||
|
..default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod flexbox;
|
pub mod flexbox;
|
||||||
|
pub mod popups;
|
||||||
pub mod texts;
|
pub mod texts;
|
||||||
|
|
||||||
pub use button::{button, pill_button};
|
pub use button::{button, pill_button};
|
||||||
pub use flexbox::Flexbox;
|
pub use flexbox::Flexbox;
|
||||||
|
pub use popups::{spawn_context_menu, spawn_popup};
|
||||||
pub use texts::{text, text_with_component};
|
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::*;
|
pub use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Creates a basic text bundle.
|
||||||
pub fn text(content: impl Into<String>, size: f32, color: Color) -> (Text, TextFont, TextColor) {
|
pub fn text(content: impl Into<String>, size: f32, color: Color) -> (Text, TextFont, TextColor) {
|
||||||
(
|
(
|
||||||
Text::new(content),
|
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(
|
pub fn text_with_component(
|
||||||
component: impl Component,
|
component: impl Component,
|
||||||
content: impl Into<String>,
|
content: impl Into<String>,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
/// Checks if the cursor is hovering over any UI element.
|
||||||
pub fn ui_blocks(
|
pub fn ui_blocks(
|
||||||
window: Single<&Window, With<PrimaryWindow>>,
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
cursor_pos: Vec2,
|
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::*;
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = GameConfig::read_config().unwrap_or(GameConfig::default());
|
let config = GameConfig::read_config().unwrap_or(GameConfig::default());
|
||||||
|
|
||||||
App::new()
|
let mut app = App::new();
|
||||||
.add_plugins((
|
|
||||||
|
app.add_plugins((
|
||||||
DefaultPlugins.set(ImagePlugin::default_nearest()),
|
DefaultPlugins.set(ImagePlugin::default_nearest()),
|
||||||
AsepriteUltraPlugin,
|
AsepriteUltraPlugin,
|
||||||
))
|
));
|
||||||
.add_plugins((FpsOverlayPlugin {
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
use bevy_dev_tools::fps_overlay::*;
|
||||||
|
app.add_plugins(FpsOverlayPlugin {
|
||||||
config: FpsOverlayConfig {
|
config: FpsOverlayConfig {
|
||||||
refresh_interval: core::time::Duration::from_millis(100),
|
refresh_interval: core::time::Duration::from_millis(100),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -20,8 +24,10 @@ fn main() {
|
|||||||
},
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
},))
|
});
|
||||||
.add_plugins((
|
}
|
||||||
|
|
||||||
|
app.add_plugins((
|
||||||
features::CorePlugin,
|
features::CorePlugin,
|
||||||
features::StartScreenPlugin,
|
features::StartScreenPlugin,
|
||||||
features::GameScreenPlugin,
|
features::GameScreenPlugin,
|
||||||
@@ -34,10 +40,13 @@ fn main() {
|
|||||||
features::UiPlugin,
|
features::UiPlugin,
|
||||||
features::InventoryPlugin,
|
features::InventoryPlugin,
|
||||||
features::ShopPlugin,
|
features::ShopPlugin,
|
||||||
))
|
features::WonderEventPlugin,
|
||||||
.insert_resource(config)
|
features::NotificationPlugin,
|
||||||
.add_systems(Startup, overwrite_default_font)
|
features::AchievementPlugin,
|
||||||
.run();
|
));
|
||||||
|
app.insert_resource(config);
|
||||||
|
app.add_systems(Startup, overwrite_default_font);
|
||||||
|
app.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn overwrite_default_font(mut fonts: ResMut<Assets<Font>>) {
|
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},
|
utils::{grid_to_world_coords, world_to_grid_coords},
|
||||||
},
|
},
|
||||||
inventory::components::{Inventory, ItemStack, ItemType},
|
inventory::components::{Inventory, ItemStack, ItemType},
|
||||||
|
notification::components::Notifications,
|
||||||
phase::components::{CurrentPhase, Phase},
|
phase::components::{CurrentPhase, Phase},
|
||||||
pom::{
|
pom::{
|
||||||
components::{GridPosition, MovingState, Pom},
|
components::{GridPosition, MovingState, Pom},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Returns the platform-specific data directory for the application.
|
||||||
pub fn get_internal_path() -> Option<PathBuf> {
|
pub fn get_internal_path() -> Option<PathBuf> {
|
||||||
let project_dirs = ProjectDirs::from("de", "demenik", "pomomon-garden");
|
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::config::components::{BerrySeedConfig, GameConfig};
|
||||||
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||||
|
use pomomon_garden::features::phase::components::SessionTracker;
|
||||||
|
|
||||||
pub fn setup_app(
|
pub fn setup_app(
|
||||||
grid_width: u32,
|
grid_width: u32,
|
||||||
@@ -65,5 +66,7 @@ pub fn setup_app(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.init_resource::<SessionTracker>();
|
||||||
|
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::fs;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use uuid::Uuid;
|
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) {
|
fn create_temp_file(content: &str) -> (std::path::PathBuf, String) {
|
||||||
let filename = format!("test_config_{}.json", Uuid::new_v4());
|
let filename = format!("test_config_{}.json", Uuid::new_v4());
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
@@ -25,7 +25,9 @@ fn test_load_valid_config() {
|
|||||||
"pom_speed": 2.0,
|
"pom_speed": 2.0,
|
||||||
"shovel_base_price": 10,
|
"shovel_base_price": 10,
|
||||||
"shovel_rate": 0.2,
|
"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::config::components::{BerrySeedConfig, GameConfig};
|
||||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
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::features::pom::actions::InteractionAction;
|
||||||
use pomomon_garden::prelude::*;
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ fn test_harvest_fully_grown() {
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
config: Res<GameConfig>| {
|
config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
InteractionAction::Harvest.execute(
|
InteractionAction::Harvest.execute(
|
||||||
(0, 0),
|
(0, 0),
|
||||||
&grid,
|
&grid,
|
||||||
@@ -50,6 +52,7 @@ fn test_harvest_fully_grown() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&config,
|
&config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -70,6 +73,10 @@ fn test_harvest_fully_grown() {
|
|||||||
let stack = app.world().entity(stack_entity).get::<ItemStack>().unwrap();
|
let stack = app.world().entity(stack_entity).get::<ItemStack>().unwrap();
|
||||||
assert_eq!(stack.item_type, ItemType::Berry);
|
assert_eq!(stack.item_type, ItemType::Berry);
|
||||||
assert_eq!(stack.amount, 5);
|
assert_eq!(stack.amount, 5);
|
||||||
|
|
||||||
|
// Check Session Tracker
|
||||||
|
let tracker = app.world().resource::<SessionTracker>();
|
||||||
|
assert_eq!(tracker.total_berries_earned, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -105,7 +112,8 @@ fn test_harvest_withered() {
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
config: Res<GameConfig>| {
|
config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
InteractionAction::Harvest.execute(
|
InteractionAction::Harvest.execute(
|
||||||
(0, 0),
|
(0, 0),
|
||||||
&grid,
|
&grid,
|
||||||
@@ -114,6 +122,7 @@ fn test_harvest_withered() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&config,
|
&config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -164,7 +173,8 @@ fn test_cannot_harvest_growing() {
|
|||||||
mut tile_query: Query<&mut TileState>,
|
mut tile_query: Query<&mut TileState>,
|
||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
config: Res<GameConfig>| {
|
config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
InteractionAction::Harvest.execute(
|
InteractionAction::Harvest.execute(
|
||||||
(0, 0),
|
(0, 0),
|
||||||
&grid,
|
&grid,
|
||||||
@@ -173,6 +183,7 @@ fn test_cannot_harvest_growing() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&config,
|
&config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use pomomon_garden::features::pom::utils::find_path;
|
|||||||
use pomomon_garden::prelude::*;
|
use pomomon_garden::prelude::*;
|
||||||
use std::collections::VecDeque;
|
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(
|
fn setup_pathfinding_app(
|
||||||
grid_width: u32,
|
grid_width: u32,
|
||||||
grid_height: u32,
|
grid_height: u32,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use bevy::ecs::system::RunSystemOnce;
|
|||||||
use pomomon_garden::features::config::components::GameConfig;
|
use pomomon_garden::features::config::components::GameConfig;
|
||||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
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::features::pom::actions::InteractionAction;
|
||||||
use pomomon_garden::prelude::*;
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ fn test_plant_seed_interaction() {
|
|||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
game_config: Res<GameConfig>| {
|
game_config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
let action = InteractionAction::Plant(seed_type.clone());
|
let action = InteractionAction::Plant(seed_type.clone());
|
||||||
action.execute(
|
action.execute(
|
||||||
(1, 1),
|
(1, 1),
|
||||||
@@ -37,6 +39,7 @@ fn test_plant_seed_interaction() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&game_config,
|
&game_config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -98,7 +101,8 @@ fn test_plant_seed_no_inventory() {
|
|||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
game_config: Res<GameConfig>| {
|
game_config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
let action = InteractionAction::Plant(seed_type.clone());
|
let action = InteractionAction::Plant(seed_type.clone());
|
||||||
action.execute(
|
action.execute(
|
||||||
(1, 1),
|
(1, 1),
|
||||||
@@ -108,6 +112,7 @@ fn test_plant_seed_no_inventory() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&game_config,
|
&game_config,
|
||||||
|
&mut session_tracker,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,9 +11,14 @@ fn test_session_tracker_focus_to_short_break() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
let timer_settings = TimerSettings::default();
|
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!(
|
assert_eq!(
|
||||||
session_tracker.completed_focus_phases, 1,
|
session_tracker.completed_focus_phases, 1,
|
||||||
@@ -37,15 +42,18 @@ fn test_session_tracker_focus_to_long_break() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
let timer_settings = TimerSettings::default();
|
let timer_settings = TimerSettings::default();
|
||||||
let mut session_tracker = SessionTracker {
|
// Simulate that grant_focus_rewards has already incremented the counter to the interval
|
||||||
completed_focus_phases: timer_settings.long_break_interval - 1,
|
let session_tracker = SessionTracker {
|
||||||
}; // To trigger long break on next phase
|
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!(
|
assert_eq!(
|
||||||
session_tracker.completed_focus_phases, timer_settings.long_break_interval,
|
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 {
|
if let Phase::Break { duration } = current_phase.0 {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -64,12 +72,14 @@ fn test_session_tracker_break_to_focus() {
|
|||||||
duration: 5.0 * 60.0,
|
duration: 5.0 * 60.0,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
let mut session_tracker = SessionTracker {
|
let session_tracker = SessionTracker {
|
||||||
completed_focus_phases: 1,
|
completed_focus_phases: 1,
|
||||||
|
total_berries_earned: 0,
|
||||||
|
total_plants_withered: 0,
|
||||||
}; // Arbitrary value, should not change
|
}; // Arbitrary value, should not change
|
||||||
let timer_settings = TimerSettings::default();
|
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!(
|
assert_eq!(
|
||||||
session_tracker.completed_focus_phases, 1,
|
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() {
|
fn test_session_tracker_not_finished_phase_no_change() {
|
||||||
// Test that nothing changes if the phase is not `Finished`
|
// Test that nothing changes if the phase is not `Finished`
|
||||||
let mut current_phase = CurrentPhase(Phase::Focus { duration: 100.0 });
|
let mut current_phase = CurrentPhase(Phase::Focus { duration: 100.0 });
|
||||||
let mut session_tracker = SessionTracker {
|
let session_tracker = SessionTracker {
|
||||||
completed_focus_phases: 0,
|
completed_focus_phases: 0,
|
||||||
|
total_berries_earned: 0,
|
||||||
|
total_plants_withered: 0,
|
||||||
};
|
};
|
||||||
let timer_settings = TimerSettings::default();
|
let timer_settings = TimerSettings::default();
|
||||||
|
|
||||||
let initial_phase = current_phase.0.clone();
|
let initial_phase = current_phase.0.clone();
|
||||||
let initial_completed_focus = session_tracker.completed_focus_phases;
|
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!(
|
assert_eq!(
|
||||||
current_phase.0, initial_phase,
|
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::config::components::GameConfig;
|
||||||
use pomomon_garden::features::grid::components::{Grid, TileState};
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
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::features::pom::actions::InteractionAction;
|
||||||
use pomomon_garden::prelude::*;
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ fn test_water_crop() {
|
|||||||
mut inventory: ResMut<Inventory>,
|
mut inventory: ResMut<Inventory>,
|
||||||
mut item_stack_query: Query<&mut ItemStack>,
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
game_config: Res<GameConfig>| {
|
game_config: Res<GameConfig>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>| {
|
||||||
let action = InteractionAction::Water;
|
let action = InteractionAction::Water;
|
||||||
action.execute(
|
action.execute(
|
||||||
(1, 1),
|
(1, 1),
|
||||||
@@ -67,6 +69,7 @@ fn test_water_crop() {
|
|||||||
&mut item_stack_query,
|
&mut item_stack_query,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&game_config,
|
&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