diff --git a/Cargo.lock b/Cargo.lock index 1c95b736ee6c90c505d3cad2ef52ea762398986b..2d2ed425816ac02d5e84902e15fb3bce4f775449 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,24 @@ dependencies = [ "bevy_internal", ] +[[package]] +name = "bevy_animation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b8073f299eb60ce9e1d60fa293b348590dd57aca8321d6859d9e7aa57d2da" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_core", + "bevy_ecs", + "bevy_hierarchy", + "bevy_math", + "bevy_reflect", + "bevy_time", + "bevy_transform", + "bevy_utils", +] + [[package]] name = "bevy_app" version = "0.9.1" @@ -232,7 +250,7 @@ dependencies = [ "js-sys", "ndk-glue", "notify", - "parking_lot", + "parking_lot 0.12.1", "serde", "thiserror", "wasm-bindgen", @@ -240,6 +258,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "bevy_audio" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a05efc6c23bef37520e44029943c65b7e8a4fe4f5e54cb3f96e63ce0b3d361" +dependencies = [ + "anyhow", + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_reflect", + "bevy_utils", + "parking_lot 0.12.1", + "rodio", +] + [[package]] name = "bevy_core" version = "0.9.1" @@ -364,6 +398,19 @@ dependencies = [ "encase_derive_impl", ] +[[package]] +name = "bevy_gilrs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4af552dad82f854b2fae24f36a389fd8ee99d65fe86ae876e854e70d53ff16d9" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_utils", + "gilrs", +] + [[package]] name = "bevy_gltf" version = "0.9.1" @@ -372,6 +419,7 @@ checksum = "e853e346ba412354e02292c7aa5b9a9dccdfa748e273b1b7ebf8f6a172f89712" dependencies = [ "anyhow", "base64", + "bevy_animation", "bevy_app", "bevy_asset", "bevy_core", @@ -428,13 +476,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c46014b7e885b1311de06b6039e448454a4db55b8d35464798ba88faa186e11" dependencies = [ + "bevy_animation", "bevy_app", "bevy_asset", + "bevy_audio", "bevy_core", "bevy_core_pipeline", "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_gilrs", "bevy_gltf", "bevy_hierarchy", "bevy_input", @@ -444,6 +495,7 @@ dependencies = [ "bevy_ptr", "bevy_reflect", "bevy_render", + "bevy_scene", "bevy_sprite", "bevy_tasks", "bevy_text", @@ -465,7 +517,7 @@ dependencies = [ "anyhow", "bevy", "kira", - "parking_lot", + "parking_lot 0.12.1", "thiserror", ] @@ -557,7 +609,7 @@ dependencies = [ "erased-serde", "glam", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "serde", "smallvec", "thiserror", @@ -610,7 +662,7 @@ dependencies = [ "image", "naga", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "regex", "serde", "smallvec", @@ -868,6 +920,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" @@ -1152,12 +1210,12 @@ dependencies = [ "ndk-context", "oboe", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "stdweb", "thiserror", "wasm-bindgen", "web-sys", - "windows", + "windows 0.37.0", ] [[package]] @@ -1179,6 +1237,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -1188,6 +1270,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "cty" version = "0.2.2" @@ -1240,6 +1328,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.5", +] + [[package]] name = "discard" version = "1.0.4" @@ -1267,6 +1368,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "encase" version = "0.4.0" @@ -1342,6 +1449,21 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb5f255b5980bb0c8cf676b675d1a99be40f316881444f44e0462eaf5df5ded" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "smallvec", + "threadpool", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -1363,6 +1485,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1379,6 +1510,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1436,6 +1580,12 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + [[package]] name = "fxhash" version = "0.2.1" @@ -1455,11 +1605,15 @@ dependencies = [ "bevy_tweening", "fastrand", "iyes_loopless", + "kayak_font", + "kayak_ui", + "ldtk_rust", "log", "micro_asset_io", "micro_banimate", "micro_musicbox", "noise", + "num-traits", "serde", "serde_json", "thiserror", @@ -1491,6 +1645,49 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gilrs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d0342acdc7b591d171212e17c9350ca02383b86d5f9af33c6e3598e03a6c57e" +dependencies = [ + "fnv", + "gilrs-core", + "log", + "uuid", + "vec_map", +] + +[[package]] +name = "gilrs-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4e9fae0d270709ce2c8aacb34f2b7293d4828ce748360ec364ccb7c1585e53" +dependencies = [ + "core-foundation", + "io-kit-sys", + "js-sys", + "libc", + "libudev-sys", + "log", + "nix 0.25.0", + "uuid", + "vec_map", + "wasm-bindgen", + "web-sys", + "windows 0.43.0", +] + [[package]] name = "glam" version = "0.22.0" @@ -1614,6 +1811,15 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "half" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554" +dependencies = [ + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -1644,6 +1850,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -1681,10 +1896,14 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", + "gif", + "jpeg-decoder", "num-rational", "num-traits", "png", "scoped_threadpool", + "tiff", ] [[package]] @@ -1741,6 +1960,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" +[[package]] +name = "io-kit-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7789f7f3c9686f96164f5109d69152de759e76e284f736bd57661c6df5091919" +dependencies = [ + "core-foundation-sys", + "mach", +] + [[package]] name = "itoa" version = "1.0.4" @@ -1788,6 +2017,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -1797,6 +2035,54 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kayak_font" +version = "0.1.0" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7" +dependencies = [ + "anyhow", + "bevy", + "image", + "nanoserde", + "num", + "num-derive", + "num-traits", + "ttf-parser", + "unicode-segmentation", + "xi-unicode", +] + +[[package]] +name = "kayak_ui" +version = "0.1.0" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7" +dependencies = [ + "bevy", + "bitflags", + "bytemuck", + "dashmap", + "indexmap", + "kayak_font", + "kayak_ui_macros", + "log", + "morphorm", + "reorder", + "resources", +] + +[[package]] +name = "kayak_ui_macros" +version = "0.1.0" +source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7" +dependencies = [ + "find-crate", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "khronos-egl" version = "4.1.0" @@ -1852,6 +2138,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "ldtk_rust" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6ec9d145e9e4265fc8d473114353c07ab84a21ea1e1ad7c86ba7fc6337aebe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libc" version = "0.2.137" @@ -1868,6 +2181,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -1949,6 +2272,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.24.0" @@ -2030,6 +2362,16 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "morphorm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f33fad4a772050ad1cac8d7add5f1e73ac2a3d581120bbdbe472fbc0c31163" +dependencies = [ + "bitflags", + "smallvec", +] + [[package]] name = "naga" version = "0.10.0" @@ -2052,6 +2394,30 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "nanoserde" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755e7965536bc54d7c9fba2df5ada5bf835b0443fd613f0a53fa199a301839d3" +dependencies = [ + "nanoserde-derive", +] + +[[package]] +name = "nanoserde-derive" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7a94da6c6181c35d043fc61c43ac96d3a5d739e7b8027f77650ba41504d6ab" + [[package]] name = "ndk" version = "0.6.0" @@ -2099,7 +2465,7 @@ dependencies = [ "ndk-macro", "ndk-sys 0.4.1+23.1.7779620", "once_cell", - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -2143,7 +2509,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2155,7 +2521,7 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2168,7 +2534,7 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -2220,6 +2586,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -2241,6 +2641,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2248,6 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", + "num-bigint", "num-integer", "num-traits", ] @@ -2261,6 +2673,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.5.7" @@ -2324,6 +2746,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -2351,6 +2782,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2358,7 +2800,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.5", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -2396,6 +2852,26 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -2446,6 +2922,30 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -2550,6 +3050,29 @@ dependencies = [ "cty", ] +[[package]] +name = "rayon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "rectangle-pack" version = "0.4.2" @@ -2597,6 +3120,23 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" +[[package]] +name = "reorder" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6672031e07dc5b839bd8be4d950eec4575e29996b538c0e96043c46bc5b56d" + +[[package]] +name = "resources" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42070ea13709eb92d2977b48c7d3bd44866fa328e14248f8d1f00d6ea14d5066" +dependencies = [ + "downcast-rs", + "fxhash", + "parking_lot 0.11.2", +] + [[package]] name = "ringbuf" version = "0.3.1" @@ -2606,6 +3146,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rodio" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb10b653d5ec0e9411a2e7d46e2c7f4046fd87d35b9955bd73ba4108d69072b5" +dependencies = [ + "cpal", + "lewton", +] + [[package]] name = "ron" version = "0.8.0" @@ -2787,6 +3337,15 @@ dependencies = [ "wayland-protocols", ] +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + [[package]] name = "spirv" version = "0.2.0+1.5.4" @@ -2987,6 +3546,41 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17def29300a156c19ae30814710d9c63cd50288a49c6fd3a10ccfbe4cf886fd" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "toml" version = "0.5.9" @@ -3087,6 +3681,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + [[package]] name = "unicode-width" version = "0.1.10" @@ -3304,6 +3904,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wgpu" version = "0.14.2" @@ -3314,7 +3920,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot", + "parking_lot 0.12.1", "raw-window-handle 0.5.0", "smallvec", "static_assertions", @@ -3340,7 +3946,7 @@ dependencies = [ "fxhash", "log", "naga", - "parking_lot", + "parking_lot 0.12.1", "profiling", "raw-window-handle 0.5.0", "smallvec", @@ -3376,7 +3982,7 @@ dependencies = [ "metal", "naga", "objc", - "parking_lot", + "parking_lot 0.12.1", "profiling", "range-alloc", "raw-window-handle 0.5.0", @@ -3442,6 +4048,21 @@ dependencies = [ "windows_x86_64_msvc 0.37.0", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + [[package]] name = "windows-sys" version = "0.36.1" @@ -3591,7 +4212,7 @@ dependencies = [ "ndk-glue", "objc", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "percent-encoding", "raw-window-handle 0.4.3", "raw-window-handle 0.5.0", diff --git a/Makefile b/Makefile index 3d8c38951e82535b0e1a6890b832a3297a239fb8..d6226624a5fd57e7b658cdb15e92f518dd766931 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,9 @@ check: pak: cd raw_assets && tar -cJf ../assets/resources.apack ./ +msdf: + msdf-atlas-gen -font raw_assets/fonts/CompassPro.ttf -type msdf -minsize 18 -format png -imageout raw_assets/fonts/CompassPro.png -json raw_assets/fonts/CompassPro.kayak_font + build-windows: clean_dist top_tail docker run --rm --name "${PROJECT_NAME}-build-windows" -v "$(CURRENT_DIRECTORY):/app" -w /app --user $(shell id -u):$(shell id -g) r.lcr.gr/microhacks/bevy-builder \ cargo build --release -p game_core --target x86_64-pc-windows-gnu diff --git a/assets/resources.apack b/assets/resources.apack index 1f14276a8d0e7aeeac57a6708122b23946fdcd1c..b5f38dd40c9edcb6b709447ec6c4d9140f8ccf35 100644 --- a/assets/resources.apack +++ b/assets/resources.apack @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e279a1551f7c02cfba8b16f358e4bf8ae62065e97d511f935984f5548a20fbf5 -size 49928 +oid sha256:dcb87c1d8969f1007611545f2b08107140ca49189883bc51fc1ac7d4b3e8d73a +size 105084 diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml index 59880345d99d8b513f08625262388a0c0b16ea1c..8fc672658d2982bc0f63bc2eed7ab2c81ef560b2 100644 --- a/game_core/Cargo.toml +++ b/game_core/Cargo.toml @@ -20,11 +20,16 @@ micro_banimate.workspace = true micro_musicbox.workspace = true micro_asset_io = { path = "../micro_asset_io" } +num-traits = "0.2.15" bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap", rev = "eb20fcaccdd253ea5bf280cac7ffc5a69b674df2" } bevy_tweening = "0.6.0" toml = "0.5.9" noise = "0.8.2" +ldtk_rust = "0.6.0" + +kayak_ui.workspace = true +kayak_font.workspace = true #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"} diff --git a/game_core/src/assets/apack_handler.rs b/game_core/src/assets/apack_handler.rs index ea3e271e2ac612e24c7003c90ff3f171c37dbc24..422ad53e118acd33da5c8eeefcc40ef1124ba931 100644 --- a/game_core/src/assets/apack_handler.rs +++ b/game_core/src/assets/apack_handler.rs @@ -12,9 +12,12 @@ use bevy::ecs::system::{SystemParam, SystemParamFetch, SystemState}; use bevy::math::vec2; use bevy::prelude::*; use bevy::render::texture::{CompressedImageFormats, ImageType}; +use kayak_font::{KayakFont, Sdf}; +use ldtk_rust::Project; use micro_asset_io::{APack, APackProcessingComplete}; use serde::{Deserialize, Serialize}; +use crate::assets::asset_types::ldtk_project::LdtkProject; use crate::assets::AssetHandles; fn file_prefix(path: &PathBuf) -> Option<String> { @@ -47,6 +50,21 @@ pub struct ManifestGenericAsset { pub path: String, } +#[derive(Deserialize, Serialize, Debug)] +#[serde(untagged)] +pub enum ManifestFontAsset { + Basic { + name: String, + path: String, + }, + MSDF { + name: String, + ttf: String, + image: String, + msdf: String, + }, +} + #[derive(Deserialize, Serialize, Debug)] pub struct ManifestImage { pub name: String, @@ -75,7 +93,9 @@ pub struct APackManifest { #[serde(default = "Vec::new")] pub spritesheets: Vec<ManifestSpriteSheet>, #[serde(default = "Vec::new")] - pub fonts: Vec<ManifestGenericAsset>, + pub fonts: Vec<ManifestFontAsset>, + #[serde(default = "Vec::new")] + pub ldtk: Vec<ManifestGenericAsset>, } fn in_world<Param: SystemParam + 'static>( @@ -157,23 +177,75 @@ pub fn handle_apack_process_events(world: &mut World) { } for font in &manifest.fonts { - let asset = pack - .get(&format!("./{}", &font.path)) - .expect("Missing asset"); + match font { + ManifestFontAsset::Basic { path, name } => { + let asset = pack.get(&format!("./{}", &path)).expect("Missing asset"); + if let Ok(font_data) = Font::try_from_bytes(asset.to_vec()) { + in_world::<ParamSet<(ResMut<Assets<Font>>, ResMut<AssetHandles>)>>( + world, + move |params: &mut ParamSet<( + ResMut<Assets<Font>>, + ResMut<AssetHandles>, + )>| { + let handle = params.p0().add(font_data); + params.p1().fonts.insert(name.clone(), handle); + }, + ); + } else { + log::warn!("Malformed font {}", path); + } + } + ManifestFontAsset::MSDF { + name, + ttf, + msdf, + image, + } => { + let image_asset = + pack.get(&format!("./{}", &image)).expect("Missing asset"); + let ttf_asset = pack.get(&format!("./{}", &ttf)).expect("Missing asset"); + let msdf_asset = pack.get(&format!("./{}", &msdf)).expect("Missing asset"); - if let Ok(font_data) = Font::try_from_bytes(asset.to_vec()) { - in_world::<ParamSet<(ResMut<Assets<Font>>, ResMut<AssetHandles>)>>( - world, - move |params: &mut ParamSet<( - ResMut<Assets<Font>>, - ResMut<AssetHandles>, - )>| { - let handle = params.p0().add(font_data); - params.p1().fonts.insert(font.name.clone(), handle); - }, - ); - } else { - log::warn!("Malformed font {}", font.path); + in_world::< + ParamSet<( + ResMut<Assets<Font>>, + ResMut<Assets<Image>>, + ResMut<Assets<KayakFont>>, + ResMut<AssetHandles>, + )>, + >( + world, + move |params: &mut ParamSet<( + ResMut<Assets<Font>>, + ResMut<Assets<Image>>, + ResMut<Assets<KayakFont>>, + ResMut<AssetHandles>, + )>| { + let ttf_handle = params + .p0() + .add(Font::try_from_bytes(ttf_asset.to_vec()).unwrap()); + + let image_handle = params.p1().add( + Image::from_buffer( + image_asset.as_slice(), + ImageType::Extension("png"), + CompressedImageFormats::all(), + true, + ) + .unwrap(), + ); + let msdf_handle = params.p2().add(KayakFont::new( + Sdf::from_bytes(msdf_asset.as_slice()), + kayak_font::ImageType::Atlas(image_handle.clone_weak()), + )); + + let mut assets = params.p3(); + assets.kayak_fonts.insert(name.clone(), msdf_handle); + assets.images.insert(name.clone(), image_handle); + assets.fonts.insert(name.clone(), ttf_handle); + }, + ) + } } } @@ -221,6 +293,33 @@ pub fn handle_apack_process_events(world: &mut World) { log::warn!("Malformed spritesheet {}", &spritesheet.path); } } + + for ldtk_entry in &manifest.ldtk { + let asset = pack + .get(&format!("./{}", &ldtk_entry.path)) + .expect("Missing asset"); + + let mut ldtk_file: Project = serde_json::from_slice(asset.as_slice()).unwrap(); + in_world::<ParamSet<(ResMut<Assets<LdtkProject>>, ResMut<AssetHandles>)>>( + world, + move |params: &mut ParamSet<( + ResMut<Assets<LdtkProject>>, + ResMut<AssetHandles>, + )>| { + let handle = params.p0().add(LdtkProject(ldtk_file)); + params.p1().ldtk.insert(ldtk_entry.name.clone(), handle); + }, + ); + } + } + + log::info!("LOADED AN APACK"); + { + let mut state: SystemState<ResMut<Assets<APack>>> = SystemState::new(world); + let mut packs = state.get_mut(world); + if let Some(mut pack) = packs.get_mut(&event.0) { + pack.loaded = true + } } } } diff --git a/game_core/src/assets/asset_types/ldtk_project.rs b/game_core/src/assets/asset_types/ldtk_project.rs new file mode 100644 index 0000000000000000000000000000000000000000..7135d2e88647d72ab89229c8dd2d5d96189d3a01 --- /dev/null +++ b/game_core/src/assets/asset_types/ldtk_project.rs @@ -0,0 +1,84 @@ +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; + +use anyhow::Error; +use bevy::asset::{AssetEvent, AssetLoader, Assets, BoxedFuture, LoadContext, LoadedAsset}; +use bevy::prelude::{EventReader, Res, ResMut, Resource}; +use bevy::reflect::TypeUuid; +use ldtk_rust::{Level, Project}; + +#[derive(TypeUuid)] +#[uuid = "eb97c508-61ea-11ed-abd3-db91dd0a6e8b"] +pub struct LdtkProject(pub Project); +impl Deref for LdtkProject { + type Target = Project; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for LdtkProject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl LdtkProject {} + +#[derive(Default)] +pub struct LdtkLoader; +impl AssetLoader for LdtkLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, anyhow::Result<(), Error>> { + Box::pin(async move { + let mut ldtk: Project = serde_json::from_slice(bytes)?; + if ldtk.external_levels { + ldtk.load_external_levels(load_context.path()); + } + load_context.set_default_asset(LoadedAsset::new(LdtkProject(ldtk))); + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["ldtk"] + } +} + +#[derive(Resource, Default)] +pub struct LevelIndex(pub HashMap<String, Level>); +impl Deref for LevelIndex { + type Target = HashMap<String, Level>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for LevelIndex { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub fn handle_ldtk_project_events( + mut events: EventReader<AssetEvent<LdtkProject>>, + assets: Res<Assets<LdtkProject>>, + mut level_index: ResMut<LevelIndex>, +) { + for event in events.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + if let Some(LdtkProject(project)) = assets.get(handle) { + for level in &project.levels { + level_index.insert( + level.identifier.clone(), + serde_json::from_value(serde_json::to_value(level).unwrap()).unwrap(), + ); + } + } + } + _ => {} + } + } +} diff --git a/game_core/src/assets/asset_types/mod.rs b/game_core/src/assets/asset_types/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..fc46ae21f63dbdec25ed5b559b89c5ebb3ae6276 --- /dev/null +++ b/game_core/src/assets/asset_types/mod.rs @@ -0,0 +1 @@ +pub mod ldtk_project; diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs index e725fbbb70a446d7c4c8ac3a5bcf9f6ace166c8d..cb7994a826dfd21513e9eecfcc7faf6334ae6719 100644 --- a/game_core/src/assets/loader.rs +++ b/game_core/src/assets/loader.rs @@ -4,9 +4,11 @@ use bevy::asset::LoadState; use bevy::ecs::system::SystemParam; use bevy::prelude::*; use bevy::reflect::TypeUuid; +use kayak_font::KayakFont; use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; +use crate::assets::asset_types::ldtk_project::LdtkProject; use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig}; #[derive(SystemParam)] @@ -14,6 +16,7 @@ pub struct AssetTypeLoader<'w, 's> { pub handles: ResMut<'w, AssetHandles>, pub asset_server: Res<'w, AssetServer>, pub atlas: ResMut<'w, Assets<TextureAtlas>>, + pub apack: ResMut<'w, Assets<APack>>, #[system_param(ignore)] marker: PhantomData<&'s usize>, } @@ -34,7 +37,7 @@ macro_rules! load_state { ($container: expr => $key: ident) => { $container .asset_server - .get_group_load_state($container.handles.$key.values().map(|f| f.id)) + .get_group_load_state($container.handles.$key.values().map(|f| f.id())) }; } @@ -57,6 +60,8 @@ impl<'w, 's> AssetTypeLoader<'w, 's> { load_basic_type!(load_audio, AudioSource => sounds); load_basic_type!(load_font, Font => fonts); load_basic_type!(load_apack, APack => apacks); + load_basic_type!(load_ldtk, LdtkProject => ldtk); + load_basic_type!(load_kayak_font, KayakFont => kayak_fonts); pub fn load_spritesheet( &mut self, @@ -97,4 +102,14 @@ impl<'w, 's> AssetTypeLoader<'w, 's> { vec![image_state, atlas_state] } + + pub fn check_apack_process_status(&self) -> bool { + self.handles + .apacks + .values() + .all(|handle| match self.apack.get(handle) { + Some(pack) => pack.processed && pack.loaded, + _ => false, + }) + } } diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs index 37ce6f9abbe8fee11a395aedeaf126468ec8f68b..e5443711f0b5305384b596a19dcbb4e488e63d09 100644 --- a/game_core/src/assets/mod.rs +++ b/game_core/src/assets/mod.rs @@ -1,30 +1,32 @@ mod apack_handler; +mod asset_types; mod loader; mod resources; mod startup; +pub use asset_types::ldtk_project::{LdtkLoader, LdtkProject, LevelIndex}; use bevy::app::{App, Plugin}; +use bevy::prelude::AddAsset; use iyes_loopless::condition::ConditionSet; use iyes_loopless::prelude::AppLooplessStateExt; pub use loader::AssetTypeLoader; pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig}; use crate::assets::apack_handler::handle_apack_process_events; +use crate::assets::asset_types::ldtk_project::handle_ldtk_project_events; use crate::system::flow::AppState; pub struct AssetsPlugin; impl Plugin for AssetsPlugin { fn build(&self, app: &mut App) { app.init_resource::<AssetHandles>() + .init_resource::<LevelIndex>() + .add_asset::<LdtkProject>() + .add_asset_loader(LdtkLoader) .add_enter_system(AppState::Preload, startup::start_preload_resources) .add_enter_system(AppState::Preload, startup::start_load_resources) .add_system(handle_apack_process_events) - .add_enter_system( - AppState::Menu, - |assets: bevy::prelude::Res<AssetHandles>| { - log::info!("{:?}", assets.images); - }, - ) + .add_system(handle_ldtk_project_events) .add_system_set( ConditionSet::new() .run_in_state(AppState::Setup) diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs index 3b317e0475cd6e75bf4334f37db84a36a12772e6..459d6ef797b678516f144b19112efa4610ba0ea7 100644 --- a/game_core/src/assets/resources.rs +++ b/game_core/src/assets/resources.rs @@ -1,9 +1,12 @@ use bevy::prelude::*; use bevy::utils::HashMap; +use kayak_font::KayakFont; use micro_asset_io::APack; use micro_musicbox::prelude::AudioSource; use micro_musicbox::utilities::{SuppliesAudio, TrackType}; +use crate::assets::asset_types::ldtk_project::LdtkProject; + #[derive(Copy, Clone, Debug)] pub struct SpriteSheetConfig { pub tile_width: usize, @@ -39,6 +42,8 @@ pub struct AssetHandles { pub sounds: HashMap<String, Handle<AudioSource>>, pub fonts: HashMap<String, Handle<Font>>, pub apacks: HashMap<String, Handle<APack>>, + pub ldtk: HashMap<String, Handle<LdtkProject>>, + pub kayak_fonts: HashMap<String, Handle<KayakFont>>, } macro_rules! fetch_wrapper { @@ -69,6 +74,8 @@ impl AssetHandles { fetch_wrapper!(sound, AudioSource => sounds); fetch_wrapper!(font, Font => fonts); fetch_wrapper!(apack, APack => apacks); + fetch_wrapper!(ldtk, LdtkProject => ldtk); + fetch_wrapper!(kayak_font, KayakFont => kayak_fonts); } impl SuppliesAudio for AssetHandles { diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs index 18641088458aee209dc9716d4f0420b1b0b0df9f..8fd402053625ec50868410678ac35078e500cb1e 100644 --- a/game_core/src/assets/startup.rs +++ b/game_core/src/assets/startup.rs @@ -19,7 +19,8 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) { pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) { let load_states = loader.get_all_load_state(); - if load_states.iter().all(|state| *state == LoadState::Loaded) { + + if loader.check_apack_process_status() { log::info!("Assets loaded successfully"); commands.insert_resource(NextState(AppState::Splash)) } diff --git a/game_core/src/graphics/mod.rs b/game_core/src/graphics/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..02567b164e2f057ddc0d31236d77cbf46225df2f --- /dev/null +++ b/game_core/src/graphics/mod.rs @@ -0,0 +1,34 @@ +use bevy::prelude::*; + +#[derive(Component, Copy, Clone)] +pub struct AlignedBackground; + +pub fn adjust_aligned_backgrounds( + mut query: Query<(&mut Transform, &Handle<Image>), With<AlignedBackground>>, + windows: Res<Windows>, + images: Res<Assets<Image>>, +) { + if let Some(window) = windows.get_primary() { + let width = window.width(); + let height = window.height(); + + for (mut transform, handle) in &mut query { + if let Some(image) = images.get(handle) { + if width > height { + let scale = image.texture_descriptor.size.width as f32 / width; + transform.scale = Vec3::splat(scale); + } else { + let scale = image.texture_descriptor.size.height as f32 / height; + transform.scale = Vec3::splat(scale); + } + } + } + } +} + +pub struct GraphicsPlugin; +impl Plugin for GraphicsPlugin { + fn build(&self, app: &mut App) { + app.add_system(adjust_aligned_backgrounds); + } +} diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs index d487f56ae4e6cfd195d84d3fa124ee1fe6d0fec9..97f8201f9a76fef42fbb3884b22251854ab5f70e 100644 --- a/game_core/src/lib.rs +++ b/game_core/src/lib.rs @@ -1,8 +1,8 @@ -extern crate core; - pub mod assets; +pub mod graphics; pub mod multiplayer; pub mod splash_screen; pub mod states; pub mod system; +pub mod ui; pub mod world; diff --git a/game_core/src/main.rs b/game_core/src/main.rs index f41b9b4ffec62f4b80e41baa2c8d82a9ed3937c5..92cafa4914079ebd706afc0613edf057818c7fa0 100644 --- a/game_core/src/main.rs +++ b/game_core/src/main.rs @@ -3,6 +3,7 @@ use bevy_ecs_tilemap::TilemapPlugin; use game_core::assets::AssetHandles; use game_core::system::flow::AppState; use game_core::system::resources::InitAppPlugins; +use game_core::ui::AdventUIPlugins; use iyes_loopless::prelude::AppLooplessStateExt; use micro_musicbox::CombinedAudioPlugins; @@ -19,5 +20,7 @@ fn main() { .add_plugin(bevy_tweening::TweeningPlugin) .add_plugin(game_core::world::WorldPlugin) .add_plugin(TilemapPlugin) + .add_plugin(game_core::graphics::GraphicsPlugin) + .add_plugins(AdventUIPlugins) .run(); } diff --git a/game_core/src/states/game_state.rs b/game_core/src/states/game_state.rs new file mode 100644 index 0000000000000000000000000000000000000000..6ffe00fa7b214c0c3bdc6769388de9b072664c09 --- /dev/null +++ b/game_core/src/states/game_state.rs @@ -0,0 +1,4 @@ +use bevy::prelude::Component; + +#[derive(Component, Debug, Default, Copy, Clone)] +pub struct Player; diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs index 23ab273af1a8ac5c9b35a3e6774fd3f70079e2bd..cf9891ce4a751ee94c203c4503ccecaa5c5a8d3b 100644 --- a/game_core/src/states/menu_state.rs +++ b/game_core/src/states/menu_state.rs @@ -6,6 +6,7 @@ use bevy_tweening::{Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween}; use iyes_loopless::state::NextState; use crate::assets::AssetHandles; +use crate::graphics::AlignedBackground; use crate::system::flow::AppState; #[derive(Component)] @@ -39,7 +40,7 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) { "Trader Tales", TextStyle { font_size: 72.0, - font: assets.font("default"), + font: assets.font("compass_pro"), color: Color::ANTIQUE_WHITE, }, ), @@ -55,7 +56,7 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) { "> Press Space <", TextStyle { font_size: 48.0, - font: assets.font("default"), + font: assets.font("compass_pro"), color: Color::ANTIQUE_WHITE, }, ), diff --git a/game_core/src/states/mod.rs b/game_core/src/states/mod.rs index a75b179f11527d0472451760455a3929f6957d0a..01a9f906629e6a20cfc43a3491d57e0ab1c8803d 100644 --- a/game_core/src/states/mod.rs +++ b/game_core/src/states/mod.rs @@ -4,6 +4,7 @@ use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; use crate::states::menu_state::go_to_game; use crate::system::flow::AppState; +mod game_state; mod menu_state; pub struct StatesPlugin; @@ -19,3 +20,5 @@ impl Plugin for StatesPlugin { ); } } + +pub use game_state::Player; diff --git a/game_core/src/system/camera.rs b/game_core/src/system/camera.rs index 072a1856af668db76a8143c1461f1325d7fd2867..0084e8b46788ea953ebf2a70a1a79b49e07ddb29 100644 --- a/game_core/src/system/camera.rs +++ b/game_core/src/system/camera.rs @@ -1,16 +1,18 @@ use bevy::app::App; -use bevy::input::mouse::MouseMotion; -use bevy::math::{Vec2, Vec3Swizzles}; +use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::math::{vec3, Vec2, Vec3Swizzles}; use bevy::prelude::{ - Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, MouseButton, - OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With, + Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, Local, MouseButton, + OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With, Without, }; use bevy::render::camera::ScalingMode; use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; use crate::system::flow::AppState; use crate::system::load_config::virtual_size; +use crate::system::utilities::{f32_max, f32_min}; use crate::system::window::WindowManager; +use crate::world::MapQuery; /// A flag component to indicate which entity should be followed by the camera #[derive(Component)] @@ -43,20 +45,33 @@ pub fn spawn_orthographic_camera(mut commands: Commands) { pub fn pan_camera( mut ev_motion: EventReader<MouseMotion>, + mut ev_scroll: EventReader<MouseWheel>, input_mouse: Res<Input<MouseButton>>, - mut query: Query<(&mut Transform, &Projection), With<GameCamera>>, + mut query: Query<(&mut Transform, &OrthographicProjection), With<GameCamera>>, windows: Res<Windows>, + mut last_mouse_pos: Local<Vec2>, ) { let mut pan = Vec2::ZERO; - if input_mouse.pressed(MouseButton::Right) { - for ev in ev_motion.iter() { - pan += ev.delta; - } - } + if let Some(mouse) = windows + .get_primary() + .and_then(|window| window.cursor_position()) + { + let previous_mouse = std::mem::replace(&mut *last_mouse_pos, mouse); + + let mouse_diff = mouse - previous_mouse; - for (mut transform, _) in &mut query { - if pan.length_squared() > 0.0 {} + if input_mouse.pressed(MouseButton::Right) { + for ev in ev_motion.iter() { + pan += ev.delta; + } + + if mouse_diff != Vec2::ZERO && mouse_diff.x < 50.0 && mouse_diff.y < 50.0 { + for (mut transform, projection) in &mut query { + transform.translation -= mouse_diff.extend(0.0); + } + } + } } } @@ -67,8 +82,12 @@ pub fn pan_camera( /// by updated to the midpoint between the player and the mouse pub fn sync_chase_camera_location( mut commands: Commands, - chased_query: Query<&Transform, With<ChaseCam>>, - camera_query: Query<(Entity, &Transform), With<GameCamera>>, + chased_query: Query<&Transform, (With<ChaseCam>, Without<GameCamera>)>, + mut camera_query: Query< + (Entity, &mut Transform, &OrthographicProjection), + (With<GameCamera>, Without<ChaseCam>), + >, + map_query: MapQuery, ) { if chased_query.is_empty() { return; @@ -85,11 +104,26 @@ pub fn sync_chase_camera_location( average_location /= count as f32; } - for (entity, location) in camera_query.iter() { - commands.entity(entity).insert(Transform { - translation: average_location.extend(location.translation.z), - ..*location - }); + for (_, mut location, proj) in camera_query.iter_mut() { + if let Some(bounds) = map_query.get_camera_bounds() { + // log::info!("BOUNDS {:?}", bounds); + + let width = proj.right - proj.left; + let height = proj.top - proj.bottom; + + let val_x = f32_max( + bounds.get_min_x(width), + f32_min(bounds.get_max_x(width), average_location.x), + ); + let val_y = f32_max( + bounds.get_min_y(height), + f32_min(bounds.get_max_y(height), average_location.y), + ); + + location.translation = vec3(val_x, val_y, location.translation.z); + } else { + location.translation = average_location.extend(location.translation.z); + } } } diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs index a989503b5a093d3b463381b4d121f5efc29be91f..46ed6f71b6c8fbb0ee06122ad2c54ac04bc16fb6 100644 --- a/game_core/src/system/load_config.rs +++ b/game_core/src/system/load_config.rs @@ -13,7 +13,7 @@ mod setup { (1280.0, 720.0) } pub fn virtual_size() -> (f32, f32) { - (640.0, 360.0) + (320.0, 180.0) } } diff --git a/game_core/src/system/utilities.rs b/game_core/src/system/utilities.rs index 180bb4226b0a162371f898f14644acc439fd5f39..1f4517c03498b76d829bf61502ee19c69819d153 100644 --- a/game_core/src/system/utilities.rs +++ b/game_core/src/system/utilities.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; use bevy::prelude::UVec2; +use num_traits::AsPrimitive; #[inline] pub fn f32_max(a: f32, b: f32) -> f32 { @@ -42,41 +43,83 @@ pub struct Indexer { } impl Indexer { - pub fn new(width: usize, height: usize) -> Self { - Indexer { width, height } + pub fn new(width: impl AsPrimitive<usize>, height: impl AsPrimitive<usize>) -> Self { + Indexer { + width: width.as_(), + height: height.as_(), + } } - pub fn index(&self, x: usize, y: usize) -> usize { - (y * self.width) + x + pub fn index(&self, x: impl AsPrimitive<usize>, y: impl AsPrimitive<usize>) -> usize { + (y.as_() * self.width) + x.as_() } - pub fn checked_index(&self, x: usize, y: usize) -> Option<usize> { + pub fn checked_index( + &self, + x: impl AsPrimitive<usize>, + y: impl AsPrimitive<usize>, + ) -> Option<usize> { if self.is_coordinate_valid(x, y) { Some(self.index(x, y)) } else { None } } - pub fn reverse(&self, index: usize) -> (usize, usize) { - (index % self.width, index / self.width) + pub fn reverse(&self, index: impl AsPrimitive<usize>) -> (usize, usize) { + (index.as_() % self.width, index.as_() / self.width) } - pub fn checked_reverse(&self, idx: usize) -> Option<(usize, usize)> { + pub fn checked_reverse(&self, idx: impl AsPrimitive<usize>) -> Option<(usize, usize)> { if self.is_index_valid(idx) { Some(self.reverse(idx)) } else { None } } - pub fn index_within(&self, idx: usize) -> bool { + pub fn index_within(&self, idx: impl AsPrimitive<usize>) -> bool { let (x, y) = self.reverse(idx); x >= 0 && x < self.width && y >= 0 && y < self.height } pub fn is_uvec2_valid(&self, point: UVec2) -> bool { - self.is_coordinate_valid(point.x as usize, point.y as usize) + self.is_coordinate_valid(point.x, point.y) + } + pub fn square_adjacent( + &self, + x: impl AsPrimitive<u32>, + y: impl AsPrimitive<u32>, + ) -> Vec<UVec2> { + let x = x.as_(); + let y = y.as_(); + + let initial_point = UVec2::new(x, y); + + let left = UVec2::new(x.saturating_sub(1), y); + let right = UVec2::new(x.saturating_add(1), y); + let top = UVec2::new(x, y.saturating_add(1)); + let bottom = UVec2::new(x, y.saturating_sub(1)); + + let mut output = Vec::with_capacity(4); + if left != initial_point { + output.push(left); + } + if right != initial_point && right.x < self.width as u32 { + output.push(right); + } + if bottom != initial_point { + output.push(bottom) + } + if top != initial_point && top.y < self.height as u32 { + output.push(top) + } + + output } - pub fn is_coordinate_valid(&self, x: usize, y: usize) -> bool { - x < self.width && y < self.height + pub fn is_coordinate_valid( + &self, + x: impl AsPrimitive<usize>, + y: impl AsPrimitive<usize>, + ) -> bool { + x.as_() < self.width && y.as_() < self.height } - pub fn is_index_valid(&self, idx: usize) -> bool { - idx < self.width * self.height + pub fn is_index_valid(&self, idx: impl AsPrimitive<usize>) -> bool { + idx.as_() < self.width * self.height } pub fn width(&self) -> usize { self.width diff --git a/game_core/src/ui/components/a_text_box.rs b/game_core/src/ui/components/a_text_box.rs new file mode 100644 index 0000000000000000000000000000000000000000..8cab85aa44bfc210c1d5b4e32aceef28fc020c4c --- /dev/null +++ b/game_core/src/ui/components/a_text_box.rs @@ -0,0 +1,525 @@ +use std::time::Instant; + +use bevy::prelude::*; +use kayak_font::{KayakFont, TextProperties}; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{ + BackgroundBundle, ClipBundle, ElementBundle, NinePatch, TextProps, TextWidgetBundle, +}; +use kayak_ui::DEFAULT_FONT; + +use crate::assets::AssetHandles; +use crate::ui::prelude::{edge_px, px, stretch, value}; + +/// Props used by the [`TextBox`] widget +#[derive(Component, PartialEq, Debug, Clone)] +pub struct ATextBoxProps { + /// If true, prevents the widget from being focused (and consequently edited) + pub disabled: bool, + /// The text to display when the user input is empty + pub placeholder: Option<String>, + /// The user input + /// + /// This is a controlled state. You _must_ set this to the value to you wish to be displayed. + /// You can use the [`on_change`] callback to update this prop as the user types. + pub value: String, + /// Set the size of the text to be rendered. This will determine element height + pub font_size: f32, +} + +impl Default for ATextBoxProps { + fn default() -> Self { + ATextBoxProps { + disabled: Default::default(), + placeholder: Default::default(), + value: Default::default(), + font_size: 18.0, + } + } +} + +#[derive(Component, Clone, PartialEq)] +pub struct ATextBoxState { + pub focused: bool, + pub graphemes: Vec<String>, + pub cursor_x: f32, + pub cursor_position: usize, + pub cursor_visible: bool, + pub cursor_last_update: Instant, + pub current_value: String, +} + +impl Default for ATextBoxState { + fn default() -> Self { + Self { + focused: Default::default(), + graphemes: Default::default(), + cursor_x: 0.0, + cursor_position: Default::default(), + cursor_visible: Default::default(), + cursor_last_update: Instant::now(), + current_value: String::new(), + } + } +} + +pub struct ATextBoxValue(pub String); + +impl Widget for ATextBoxProps {} + +/// A widget that displays a text input field +/// A text box allows users to input text. +/// This text box is fairly simple and only supports basic input. +/// +#[derive(Bundle)] +pub struct ATextBoxWidget { + pub text_box: ATextBoxProps, + pub styles: KStyle, + pub computed_styles: ComputedStyles, + pub on_event: OnEvent, + pub on_layout: OnLayout, + pub on_change: OnChange, + pub focusable: Focusable, + pub widget_name: WidgetName, +} + +impl Default for ATextBoxWidget { + fn default() -> Self { + Self { + text_box: Default::default(), + styles: Default::default(), + computed_styles: ComputedStyles::default(), + on_event: Default::default(), + on_layout: Default::default(), + on_change: Default::default(), + focusable: Default::default(), + widget_name: ATextBoxProps::default().get_name(), + } + } +} + +pub fn render_text_box_widget( + In((widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + mut query: Query<( + &KStyle, + &mut ComputedStyles, + &ATextBoxProps, + &mut OnEvent, + &OnChange, + )>, + mut state_query: ParamSet<(Query<&ATextBoxState>, Query<&mut ATextBoxState>)>, + font_assets: Res<Assets<KayakFont>>, + font_mapping: Res<FontMapping>, + asset_handles: Res<AssetHandles>, +) -> bool { + if let Ok((styles, mut computed_styles, text_box, mut on_event, on_change)) = + query.get_mut(entity) + { + let state_entity = widget_context.use_state::<ATextBoxState>( + &mut commands, + entity, + ATextBoxState { + current_value: text_box.value.clone(), + ..ATextBoxState::default() + }, + ); + + let mut is_different = false; + if let Ok(state) = state_query.p0().get(state_entity) { + if state.current_value != text_box.value { + is_different = true; + } + } + + let style_font = styles.font.clone(); + let font_size = text_box.font_size; + let inner_height = get_font_size_line_height(font_size); + let element_height = inner_height + 24.0; + + if is_different { + if let Ok(mut state) = state_query.p1().get_mut(state_entity) { + state.current_value = text_box.value.clone(); + // Update graphemes + set_graphemes(&mut state, &font_assets, &font_mapping, &style_font); + + state.cursor_position = state.graphemes.len(); + + set_new_cursor_position( + &mut state, + &font_assets, + &font_mapping, + &style_font, + font_size, + ); + } + } + + if let Ok(state) = state_query.p0().get(state_entity) { + // let nine_vals = NinePatch { + // handle: asset_handles.image("basic_panel"), + // border: Edge::all(12.0), + // }; + + *computed_styles = KStyle::default() + // Required styles + .with_style(KStyle { + render_command: value(match state.focused { + true => RenderCommand::NinePatch { + handle: asset_handles.image("panel_text_active"), + border: Edge::all(12.0), + }, + false => RenderCommand::NinePatch { + handle: asset_handles.image("panel_text_idle"), + border: Edge::all(12.0), + } + }), + ..Default::default() + }) + // Apply any prop-given styles + .with_style(styles) + // If not set by props, apply these styles + .with_style(KStyle { + height: px(element_height), + padding: edge_px(12.0), + // cursor: CursorIcon::Text.into(), + ..Default::default() + }) + .into(); + + let background_styles = KStyle { + render_command: StyleProp::Value(RenderCommand::Quad), + height: px(get_font_size_line_height(font_size)), + padding_left: px(5.0), + padding_right: px(5.0), + padding_bottom: px(4.0), + ..Default::default() + }; + + let cloned_on_change = on_change.clone(); + + *on_event = OnEvent::new( + move |In((event_dispatcher_context, _, mut event, _entity)): In<( + EventDispatcherContext, + WidgetState, + Event, + Entity, + )>, + font_assets: Res<Assets<KayakFont>>, + font_mapping: Res<FontMapping>, + mut state_query: Query<&mut ATextBoxState>| { + match event.event_type { + EventType::KeyDown(key_event) => { + if key_event.key() == KeyCode::Right { + if let Ok(mut state) = state_query.get_mut(state_entity) { + if state.cursor_position < state.graphemes.len() { + state.cursor_position += 1; + } + set_new_cursor_position( + &mut state, + &font_assets, + &font_mapping, + &style_font, + font_size, + ); + } + } + if key_event.key() == KeyCode::Left { + if let Ok(mut state) = state_query.get_mut(state_entity) { + if state.cursor_position > 0 { + state.cursor_position -= 1; + } + set_new_cursor_position( + &mut state, + &font_assets, + &font_mapping, + &style_font, + font_size, + ); + } + } + } + EventType::CharInput { c } => { + if let Ok(mut state) = state_query.get_mut(state_entity) { + let cloned_on_change = cloned_on_change.clone(); + if !state.focused { + return (event_dispatcher_context, event); + } + let cursor_pos = state.cursor_position; + if is_backspace_key(c) { + if !state.current_value.is_empty() { + let char_pos: usize = state.graphemes[0..cursor_pos - 1] + .iter() + .map(|g| g.len()) + .sum(); + state.current_value.remove(char_pos); + state.cursor_position -= 1; + } + } else if is_delete_key(c) { + if state.cursor_position < state.graphemes.len() { + let char_pos: usize = state.graphemes[0..cursor_pos] + .iter() + .map(|g| g.len()) + .sum(); + state.current_value.remove(char_pos); + // state.cursor_position -= 1; + } + } else if !c.is_control() { + let char_pos: usize = state.graphemes[0..cursor_pos] + .iter() + .map(|g| g.len()) + .sum(); + state.current_value.insert(char_pos, c); + + state.cursor_position += 1; + } + + // Update graphemes + set_graphemes(&mut state, &font_assets, &font_mapping, &style_font); + + set_new_cursor_position( + &mut state, + &font_assets, + &font_mapping, + &style_font, + font_size, + ); + cloned_on_change.set_value(state.current_value.clone()); + event.add_system(cloned_on_change); + } + } + EventType::Focus => { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.focused = true; + // Update graphemes + set_graphemes(&mut state, &font_assets, &font_mapping, &style_font); + + state.cursor_position = state.graphemes.len(); + + set_new_cursor_position( + &mut state, + &font_assets, + &font_mapping, + &style_font, + font_size, + ); + } + } + EventType::Blur => { + if let Ok(mut state) = state_query.get_mut(state_entity) { + state.focused = false; + } + } + _ => {} + } + (event_dispatcher_context, event) + }, + ); + + let cursor_styles = KStyle { + background_color: Color::ANTIQUE_WHITE.into(), + position_type: value(KPositionType::SelfDirected), + top: px(4.0), + left: px(state.cursor_x), + width: px(3.0), + height: px(inner_height - 8.0), + ..Default::default() + }; + + let (text_value, text_color) = if !state.focused && text_box.value.is_empty() { + ( + text_box.placeholder.clone().unwrap_or_default(), + Color::GRAY, + ) + } else { + (state.current_value.clone(), Color::WHITE) + }; + + let text_styles = KStyle { + color: value(text_color), + top: stretch(1.0), + bottom: px(4.0), + ..Default::default() + }; + + let shift = if let Some(layout) = widget_context.get_layout(entity) { + let font_handle = match &styles.font { + StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(), + _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(), + }; + if let Some(font) = font_assets.get(&font_handle) { + let string_to_cursor = state.graphemes[0..state.cursor_position].join(""); + let measurement = font.measure( + &string_to_cursor, + TextProperties { + font_size, + line_height: get_font_size_line_height(font_size), + max_size: (10000.0, get_font_size_line_height(font_size) + 4.0), + alignment: Alignment::Start, + tab_size: 4, + }, + ); + if measurement.size().0 > layout.width { + (layout.width - measurement.size().0) - 20.0 + } else { + 0.0 + } + } else { + 0.0 + } + } else { + 0.0 + }; + + let scroll_styles = KStyle { + position_type: value(KPositionType::SelfDirected), + padding_left: stretch(0.0), + padding_right: stretch(0.0), + padding_bottom: stretch(1.0), + padding_top: stretch(1.0), + left: px(shift), + ..Default::default() + }; + + let parent_id = Some(entity); + rsx! { + <BackgroundBundle styles={background_styles}> + <ClipBundle styles={KStyle { + height: px(get_font_size_line_height(font_size)), + padding_left: stretch(0.0), + padding_right: stretch(0.0), + ..Default::default() + }}> + <ElementBundle styles={scroll_styles}> + <TextWidgetBundle + styles={text_styles} + text={TextProps { + content: text_value, + size: font_size, + line_height: Some(get_font_size_line_height(font_size)), + word_wrap: false, + ..Default::default() + }} + /> + { + if state.focused && state.cursor_visible { + constructor! { + <BackgroundBundle styles={cursor_styles} /> + } + } + } + </ElementBundle> + </ClipBundle> + </BackgroundBundle> + } + } + } + + true +} + +/// Checks if the given character contains the "Backspace" sequence +/// +/// Context: [Wikipedia](https://en.wikipedia.org/wiki/Backspace#Common_use) +fn is_backspace_key(c: char) -> bool { + c == '\u{8}' +} + +fn is_delete_key(c: char) -> bool { + c == '\u{7f}' +} + +fn set_graphemes( + state: &mut ATextBoxState, + font_assets: &Res<Assets<KayakFont>>, + font_mapping: &FontMapping, + style_font: &StyleProp<String>, +) { + let font_handle = match style_font { + StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(), + _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(), + }; + + if let Some(font) = font_assets.get(&font_handle) { + state.graphemes = font + .get_graphemes(&state.current_value) + .iter() + .map(|s| s.to_string()) + .collect::<Vec<_>>(); + } +} + +fn get_single_grapheme_length( + font_assets: &Res<Assets<KayakFont>>, + font_mapping: &FontMapping, + style_font: &StyleProp<String>, + text: &String, +) -> usize { + let font_handle = match style_font { + StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(), + _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(), + }; + + if let Some(font) = font_assets.get(&font_handle) { + let graphemes = font.get_graphemes(&text); + return graphemes[0].len(); + } + + 0 +} + +fn set_new_cursor_position( + state: &mut ATextBoxState, + font_assets: &Res<Assets<KayakFont>>, + font_mapping: &FontMapping, + style_font: &StyleProp<String>, + font_size: f32, +) { + let font_handle = match style_font { + StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(), + _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(), + }; + + if let Some(font) = font_assets.get(&font_handle) { + let string_to_cursor = state.graphemes[0..state.cursor_position].join(""); + let measurement = font.measure( + &string_to_cursor, + TextProperties { + font_size, + line_height: get_font_size_line_height(font_size), + max_size: (10000.0, get_font_size_line_height(font_size) + 4.0), + alignment: Alignment::Start, + tab_size: 4, + }, + ); + + state.cursor_x = measurement.size().0; + } +} + +pub fn get_font_size_line_height(size: f32) -> f32 { + if size <= 20.0 { + size + 2.0 + } else { + size + 4.0 + } +} + +pub fn cursor_animation_system( + mut state_query: ParamSet<(Query<(Entity, &ATextBoxState)>, Query<&mut ATextBoxState>)>, +) { + let mut should_update = Vec::new(); + + for (entity, state) in state_query.p0().iter() { + if state.cursor_last_update.elapsed().as_secs_f32() > 0.5 && state.focused { + should_update.push(entity); + } + } + + for state_entity in should_update.drain(..) { + if let Ok(mut state) = state_query.p1().get_mut(state_entity) { + state.cursor_last_update = Instant::now(); + state.cursor_visible = !state.cursor_visible; + } + } +} diff --git a/game_core/src/ui/components/button.rs b/game_core/src/ui/components/button.rs new file mode 100644 index 0000000000000000000000000000000000000000..201720adbc4f38b6a59131819881ebafdc386f33 --- /dev/null +++ b/game_core/src/ui/components/button.rs @@ -0,0 +1,194 @@ +use bevy::prelude::*; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{NinePatch, NinePatchBundle, TextProps, TextWidgetBundle}; +use kayak_ui::DEFAULT_FONT; +use num_traits::AsPrimitive; + +use crate::assets::AssetHandles; +use crate::parent_widget; +use crate::ui::prelude::{edge_px, px, stretch, val_auto, value}; + +#[derive(Component, Clone, PartialEq, Default)] +pub struct ButtonWidgetProps { + pub text: String, + pub font_size: f32, + pub is_disabled: bool, + pub is_fixed: bool, +} + +impl ButtonWidgetProps { + pub fn text(text: impl ToString, font_size: impl AsPrimitive<f32>) -> Self { + Self { + text: text.to_string(), + font_size: font_size.as_(), + ..Default::default() + } + } +} + +pub fn button_props(text: impl ToString, is_disabled: bool, is_fixed: bool) -> ButtonWidgetProps { + ButtonWidgetProps { + text: text.to_string(), + is_fixed, + is_disabled, + font_size: 32.0, + } +} + +impl Widget for ButtonWidgetProps {} + +parent_widget!(ButtonWidgetProps => ButtonWidget); + +#[derive(Component, PartialEq, Clone, Default)] +pub struct ButtonWidgetState { + pub is_pressed: bool, + pub is_hovered: bool, +} + +pub fn render_button_widget( + In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + state_query: Query<&ButtonWidgetState>, + mut query: Query<(&ButtonWidgetProps, &KChildren, &mut ComputedStyles, &KStyle)>, + assets: Res<AssetHandles>, +) -> bool { + if let Ok((props, children, mut computed, style)) = query.get_mut(entity) { + let state_entity = + widget_context.use_state(&mut commands, entity, ButtonWidgetState::default()); + + let parent_id = Some(entity); + + if let Ok(state) = state_query.get(state_entity) { + let events = OnEvent::new( + move |In((event_dispatcher_context, _, mut event, _)): In<( + EventDispatcherContext, + WidgetState, + Event, + Entity, + )>, + mut params: ParamSet<( + Query<&ButtonWidgetProps>, + Query<&mut ButtonWidgetState>, + )>| { + let widget_props = match params.p0().get(entity) { + Ok(p) => p.clone(), + Err(..) => return (event_dispatcher_context, event), + }; + + if let Ok(mut state) = params.p1().get_mut(state_entity) { + match &event.event_type { + EventType::Hover(..) | EventType::MouseIn(..) => { + if !widget_props.is_disabled { + state.is_hovered = true; + } + } + EventType::MouseOut(..) => { + state.is_hovered = false; + state.is_pressed = false; + } + EventType::MouseDown(..) => { + if !widget_props.is_disabled { + state.is_pressed = true; + } + } + EventType::MouseUp(..) => { + state.is_pressed = false; + } + EventType::Click(..) => { + if widget_props.is_disabled { + event.prevent_default(); + event.stop_propagation(); + } + } + _ => {} + } + } + + (event_dispatcher_context, event) + }, + ); + + let button_height = props.font_size + 2.0 + 24.0; // + 8.0; + + let nine_vals = if props.is_disabled { + NinePatch { + handle: assets.image("button_disabled"), + border: Edge::all(12.0), + } + } else if state.is_pressed { + NinePatch { + handle: assets.image("button_down"), + border: Edge::all(12.0), + } + } else if state.is_hovered { + NinePatch { + handle: assets.image("button_active"), + border: Edge::all(12.0), + } + } else { + NinePatch { + handle: assets.image("button_idle"), + border: Edge::all(12.0), + } + }; + + let padding = if state.is_pressed { + StyleProp::Value(Edge::new( + Units::Pixels(6.0), + Units::Stretch(1.0), + Units::Pixels(16.0), + Units::Stretch(1.0), + )) + } else { + StyleProp::Value(Edge::new( + Units::Pixels(3.0), + Units::Stretch(1.0), + Units::Pixels(19.0), + Units::Stretch(1.0), + )) + }; + + *computed = KStyle { + render_command: value(RenderCommand::Layout), + min_height: px(32.0), + min_width: px(32.0), + height: px(button_height), + padding: value(Edge::all(Units::Stretch(0.0))), + ..Default::default() + } + .with_style(style) + .into(); + + let ninepatch_styles = KStyle { + padding, + ..Default::default() + }; + + let text_style = KStyle { + color: value(Color::BLACK), + ..Default::default() + }; + + rsx! { + <NinePatchBundle + on_event={events} + nine_patch={nine_vals} + styles={ninepatch_styles} + > + <TextWidgetBundle + text={TextProps { + content: props.text.clone(), + word_wrap: false, + subpixel: true, + size: props.font_size, + line_height: Some(props.font_size + 2.0), + ..Default::default() + }} + styles={text_style} + /> + </NinePatchBundle> + } + } + } + true +} diff --git a/game_core/src/ui/components/debug_info.rs b/game_core/src/ui/components/debug_info.rs new file mode 100644 index 0000000000000000000000000000000000000000..8e5e9010f66fdf69327485c5aa6bca25e9fd244c --- /dev/null +++ b/game_core/src/ui/components/debug_info.rs @@ -0,0 +1,70 @@ +// use kayak_ui::widgets::{Element, Text}; +use bevy::prelude::*; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{ElementBundle, TextWidgetBundle}; + +use crate::ui::prelude::{px, simple_text, stretch, val_auto, value}; +use crate::{basic_widget, empty_props}; + +empty_props!(DebugInfoProps); +basic_widget!(DebugInfoProps => DebugInfoWidget); + +pub fn render_debug_info( + In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, +) -> bool { + let parent_id = Some(entity); + static VERSION: &str = env!("CARGO_PKG_VERSION"); + + let element_style = KStyle { + position_type: value(KPositionType::SelfDirected), + right: px(20.0), + bottom: px(20.0), + left: stretch(1.0), + top: stretch(1.0), + width: val_auto(), + height: val_auto(), + ..Default::default() + }; + + rsx! { + <ElementBundle styles={element_style}> + <TextWidgetBundle text={simple_text(VERSION, "mono", 32.0)} /> + </ElementBundle> + } + + true +} + +// #[widget] +// pub fn DebugInfo() { +// // let f = WidgetN +// let container_style = Style { +// position_type: PositionType::SelfDirected.into(), +// bottom: px(20.0), +// right: px(20.0), +// left: stretch(1.0), +// top: stretch(1.0), +// width: px(120.0), +// height: Units::Auto.into(), +// ..Default::default() +// }; +// let text_style = Style { +// color: Color::WHITE.into(), +// font_size: (24.0).into(), +// left: stretch(1.0), +// font: context +// .get_font_id("mono") +// .map(StyleProp::Value) +// .unwrap_or(StyleProp::Unset), +// ..Default::default() +// }; +// +// let val = env!("CARGO_PKG_VERSION"); +// +// rsx! { +// <Element styles={Some(container_style)}> +// <Text styles={Some(text_style)} content={format!("v{}", val)} /> +// </Element> +// } +// } diff --git a/game_core/src/ui/components/image_button.rs b/game_core/src/ui/components/image_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..da5eb6dd1974656655f3dc0aad42fa60d7240fe2 --- /dev/null +++ b/game_core/src/ui/components/image_button.rs @@ -0,0 +1,252 @@ +use bevy::prelude::*; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{ + KImage, KImageBundle, NinePatch, NinePatchBundle, TextProps, TextWidgetBundle, + TextureAtlasBundle, TextureAtlasProps, +}; +use kayak_ui::DEFAULT_FONT; + +use crate::assets::AssetHandles; +use crate::parent_widget; +use crate::ui::prelude::{edge_px, px, stretch, value}; + +#[derive(Component, Clone, Eq, PartialEq, Default)] +pub enum ImageButtonContent { + Image(String), + Atlas(String, usize), + #[default] + None, +} + +#[derive(Component, Clone, Eq, PartialEq, Default)] +pub struct ImageButtonWidgetProps { + pub content: ImageButtonContent, + pub is_disabled: bool, + pub is_fixed: bool, +} + +impl ImageButtonWidgetProps { + pub fn image(name: impl ToString) -> Self { + Self { + content: ImageButtonContent::Image(name.to_string()), + is_disabled: false, + is_fixed: false, + } + } + pub fn atlas(name: impl ToString, index: usize) -> Self { + Self { + content: ImageButtonContent::Atlas(name.to_string(), index), + is_disabled: false, + is_fixed: false, + } + } +} + +impl Widget for ImageButtonWidgetProps {} + +parent_widget!(ImageButtonWidgetProps => ImageButtonWidget); + +#[derive(Component, PartialEq, Clone, Default)] +pub struct ImageButtonWidgetState { + pub is_pressed: bool, + pub is_hovered: bool, +} + +pub struct ButtonClickEvent { + pub button: Entity, +} + +pub fn render_image_button_widget( + In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + state_query: Query<&ImageButtonWidgetState>, + mut query: Query<( + &ImageButtonWidgetProps, + &KChildren, + &mut ComputedStyles, + &KStyle, + )>, + assets: Res<AssetHandles>, + atlass: Res<Assets<TextureAtlas>>, +) -> bool { + if let Ok((props, children, mut computed, style)) = query.get_mut(entity) { + let state_entity = + widget_context.use_state(&mut commands, entity, ImageButtonWidgetState::default()); + + let parent_id = Some(entity); + + if let Ok(state) = state_query.get(state_entity) { + let events = OnEvent::new( + move |In((event_dispatcher_context, _, mut event, _)): In<( + EventDispatcherContext, + WidgetState, + Event, + Entity, + )>, + mut params: ParamSet<( + Query<&ImageButtonWidgetProps>, + Query<&mut ImageButtonWidgetState>, + )>| { + let widget_props = match params.p0().get(entity) { + Ok(p) => p.clone(), + Err(..) => return (event_dispatcher_context, event), + }; + + if let Ok(mut state) = params.p1().get_mut(state_entity) { + match &event.event_type { + EventType::Hover(..) | EventType::MouseIn(..) => { + if !widget_props.is_disabled { + state.is_hovered = true; + } + } + EventType::MouseOut(..) => { + state.is_hovered = false; + state.is_pressed = false; + } + EventType::MouseDown(..) => { + if !widget_props.is_disabled { + state.is_pressed = true; + } + } + EventType::MouseUp(..) => { + state.is_pressed = false; + } + EventType::Click(..) => { + if widget_props.is_disabled { + event.prevent_default(); + event.stop_propagation(); + } + } + _ => {} + } + } + + (event_dispatcher_context, event) + }, + ); + + let nine_vals = if props.is_disabled { + NinePatch { + handle: assets.image("button_disabled"), + border: Edge::all(3.0), + } + } else if state.is_pressed { + NinePatch { + handle: assets.image("button_down"), + border: Edge::all(3.0), + } + } else if state.is_hovered { + NinePatch { + handle: assets.image("button_active"), + border: Edge::all(3.0), + } + } else { + NinePatch { + handle: assets.image("button_idle"), + border: Edge::all(3.0), + } + }; + + let padding = if state.is_pressed { + StyleProp::Value(Edge::new( + Units::Pixels(12.0), + Units::Pixels(8.0), + Units::Pixels(12.0), + Units::Pixels(8.0), + )) + } else { + StyleProp::Value(Edge::new( + Units::Pixels(8.0), + Units::Pixels(8.0), + Units::Pixels(16.0), + Units::Pixels(8.0), + )) + }; + + // let styles = KStyle { + // padding, + // ..Default::default() + // } + // .with_style(style) + // .with_style(KStyle { + // width: stretch(1.0), + // height: px(48.0), + // bottom: px(20.0), + // min_height: px(32.0), + // min_width: px(48.0), + // ..Default::default() + // }); + + *computed = KStyle { + render_command: value(RenderCommand::Layout), + min_height: px(32.0), + min_width: px(32.0), + padding: value(Edge::all(Units::Stretch(0.0))), + ..Default::default() + } + .with_style(style) + .into(); + + let ninepatch_styles = KStyle { + padding, + ..Default::default() + }; + + let image_styles = KStyle { + background_color: Color::RED.into(), + width: stretch(1.0), + height: stretch(1.0), + // padding_left: px(12.0), + // padding_right: px(12.0), + // padding_top: px(20.0), + // padding_bottom: px(20.0), + ..Default::default() + }; + + rsx! { + <NinePatchBundle + on_event={events} + nine_patch={nine_vals} + styles={ninepatch_styles} + > + {match &props.content { + ImageButtonContent::Image(name) => { + let handle = assets.image(name); + + constructor! { + <KImageBundle + image={KImage(handle)} + styles={image_styles} + /> + } + }, + ImageButtonContent::Atlas(name, index) => { + let atlas_handle = assets.atlas(name); + let image_handle = assets.image(name); + if let Some(atlas) = atlass.get(&atlas_handle) { + let rect = atlas.textures[*index]; + let position = rect.min; + let tile_size = rect.max - rect.min; + + constructor! { + <TextureAtlasBundle + atlas={TextureAtlasProps { + tile_size, + position, + handle: image_handle, + }} + styles={image_styles} + /> + } + + } + + }, + ImageButtonContent::None => {} + } } + </NinePatchBundle> + } + } + } + true +} diff --git a/game_core/src/ui/components/mod.rs b/game_core/src/ui/components/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..fe7558c26d5fd8de55f1202afe9c94b3bb9b242e --- /dev/null +++ b/game_core/src/ui/components/mod.rs @@ -0,0 +1,21 @@ +/// Almost all of these base components are copy/pasted from Advent Realms, but will soon be published +/// in the `advent_ui` package. Take a peak! +mod a_text_box; +mod button; +mod debug_info; +mod image_button; +mod panel; +mod v_divider; + +pub use self::a_text_box::{ + cursor_animation_system, render_text_box_widget, ATextBoxProps, ATextBoxState, ATextBoxWidget, +}; +pub use self::button::{ + button_props, render_button_widget, ButtonWidget, ButtonWidgetProps, ButtonWidgetState, +}; +pub use self::debug_info::{render_debug_info, DebugInfoProps, DebugInfoWidget}; +pub use self::image_button::{ + render_image_button_widget, ImageButtonWidget, ImageButtonWidgetProps, ImageButtonWidgetState, +}; +pub use self::panel::{render_panel_widget, PanelProps, PanelVariant, PanelWidget}; +pub use self::v_divider::{render_v_divider, VDividerWidget, VDividerWidgetProps}; diff --git a/game_core/src/ui/components/panel.rs b/game_core/src/ui/components/panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..45eca813056217e17bdb120711ab3046e25b3177 --- /dev/null +++ b/game_core/src/ui/components/panel.rs @@ -0,0 +1,125 @@ +use bevy::prelude::*; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{NinePatch, NinePatchBundle}; + +use crate::assets::AssetHandles; +use crate::ui::prelude::{edge_px, px, stretch, value}; +use crate::{basic_widget, empty_props, parent_widget}; + +#[derive(Default, Copy, Clone, Eq, PartialEq)] +pub enum PanelVariant { + #[default] + Regular, + Simple, +} + +#[derive(Component, Default, Clone, PartialEq, Eq)] +pub struct PanelProps { + pub variant: PanelVariant, +} + +impl Widget for PanelProps {} + +parent_widget!(PanelProps => PanelWidget); + +pub fn render_panel_widget( + In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + mut query: Query<(&PanelProps, &KChildren, &mut ComputedStyles, &KStyle)>, + assets: Res<AssetHandles>, +) -> bool { + if let Ok((props, children, mut computed, style)) = query.get_mut(entity) { + let parent_id = Some(entity); + let (handle, edge_size) = match props.variant { + PanelVariant::Regular => (assets.image("panel"), 16.0), + PanelVariant::Simple => (assets.image("scroll_panel"), 44.0), + }; + + *computed = KStyle { + render_command: value(RenderCommand::Layout), + min_height: px(edge_size * 2.0 + 8.0), + min_width: px(edge_size * 2.0 + 8.0), + padding: value(Edge::all(Units::Stretch(0.0))), + ..Default::default() + } + .with_style(style) + .into(); + + let inner_style = match &style.padding { + StyleProp::Unset => KStyle { + padding: edge_px(edge_size), + ..Default::default() + }, + pad => KStyle { + padding: pad.clone(), + ..Default::default() + }, + }; + + rsx! { + <NinePatchBundle + nine_patch={NinePatch { + handle, + border: Edge::all(edge_size), + }} + styles={inner_style} + children={children.clone()} + /> + } + } + true +} + +// #[widget] +// pub fn Panel(props: PanelProps) { +// let styles = Some( +// Style::default() +// .with_style(Style { +// min_height: Units::Pixels(72.0).into(), +// min_width: Units::Pixels(72.0).into(), +// ..Default::default() +// }) +// .with_style(props.get_styles()) +// .with_style(Style { +// color: Color::WHITE.into(), +// padding: StyleProp::Value(Edge::all(Units::Pixels(32.0))), +// ..Default::default() +// }), +// ); +// +// let children = props.get_children(); +// let patch = context.get_ninepatch("panel"); +// +// rsx! { +// <NinePatch border={Edge::all(4.0 * 8.0)} handle={patch} styles={styles}> +// { children } +// </NinePatch> +// } +// } +// +// #[widget] +// pub fn BasicPanel(props: PanelProps) { +// let styles = Some( +// Style::default() +// .with_style(Style { +// min_height: Units::Pixels(28.0).into(), +// min_width: Units::Pixels(28.0).into(), +// ..Default::default() +// }) +// .with_style(props.get_styles()) +// .with_style(Style { +// color: Color::WHITE.into(), +// padding: StyleProp::Value(Edge::all(Units::Pixels(12.0))), +// ..Default::default() +// }), +// ); +// +// let children = props.get_children(); +// let patch = context.get_ninepatch("basic_panel"); +// +// rsx! { +// <NinePatch border={Edge::all(12.0)} handle={patch} styles={styles} on_event={props.get_on_event()} on_layout={props.get_on_layout()}> +// { children } +// </NinePatch> +// } +// } diff --git a/game_core/src/ui/components/v_divider.rs b/game_core/src/ui/components/v_divider.rs new file mode 100644 index 0000000000000000000000000000000000000000..7017566b7c31c681678d2c229240c26e5ebd73de --- /dev/null +++ b/game_core/src/ui/components/v_divider.rs @@ -0,0 +1,49 @@ +use bevy::prelude::*; +use kayak_ui::prelude::{ + ComputedStyles, KStyle, KayakWidgetContext, RenderCommand, StyleProp, Widget, +}; + +use crate::basic_widget; +use crate::ui::prelude::{px, stretch, value}; + +#[derive(Component, Debug, Clone, PartialEq)] +pub struct VDividerWidgetProps { + pub height: f32, + pub padding: f32, + pub color: Color, +} + +impl Default for VDividerWidgetProps { + fn default() -> Self { + Self { + height: 1.0, + padding: 5.0, + color: Color::ANTIQUE_WHITE, + } + } +} + +impl Widget for VDividerWidgetProps {} + +basic_widget!(VDividerWidgetProps => VDividerWidget); + +pub fn render_v_divider( + In((_, entity)): In<(KayakWidgetContext, Entity)>, + _: Commands, + mut query: Query<(&VDividerWidgetProps, &KStyle, &mut ComputedStyles)>, +) -> bool { + if let Ok((props, style, mut computed)) = query.get_mut(entity) { + *computed = KStyle { + render_command: value(RenderCommand::Quad), + background_color: StyleProp::Value(props.color), + height: px(props.height), + top: px(props.padding), + bottom: px(props.padding), + width: stretch(1.0), + ..Default::default() + } + .with_style(style) + .into(); + } + true +} diff --git a/game_core/src/ui/mod.rs b/game_core/src/ui/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d8151710891543860ee6a0c5b9b2be90ac90ca83 --- /dev/null +++ b/game_core/src/ui/mod.rs @@ -0,0 +1,123 @@ +use bevy::app::{PluginGroup, PluginGroupBuilder}; +use kayak_ui::prelude::KayakContextPlugin; +use kayak_ui::widgets::KayakWidgets; + +// pub mod clrs; +pub mod components; +pub mod screens; +pub mod utilities; +// pub mod widgets; +pub mod sync; + +pub mod prelude { + use bevy::prelude::FromReflect; + use kayak_ui::prelude::{Alignment, Edge, StyleProp, Units}; + + #[inline(always)] + pub fn px(val: f32) -> StyleProp<Units> { + StyleProp::Value(Units::Pixels(val)) + } + + #[inline(always)] + pub fn edge_px(val: f32) -> StyleProp<Edge<Units>> { + StyleProp::Value(Edge::all(Units::Pixels(val))) + } + + #[inline(always)] + pub fn pct(val: f32) -> StyleProp<Units> { + StyleProp::Value(Units::Percentage(val)) + } + + #[inline(always)] + pub fn stretch(val: f32) -> StyleProp<Units> { + StyleProp::Value(Units::Stretch(val)) + } + + #[inline(always)] + pub fn val_auto() -> StyleProp<Units> { + StyleProp::Value(Units::Auto) + } + + #[inline(always)] + pub fn value<T: Clone + Default + FromReflect>(val: T) -> StyleProp<T> { + StyleProp::Value(val) + } + + #[inline(always)] + pub fn simple_text<Value: ToString, Font: ToString>( + value: Value, + font: Font, + size: f32, + ) -> kayak_ui::widgets::TextProps { + kayak_ui::widgets::TextProps { + content: value.to_string(), + font: Some(font.to_string()), + show_cursor: true, + size, + alignment: Alignment::Start, + ..Default::default() + } + } +} + +mod _config { + use bevy::app::{App, Plugin}; + use bevy::prelude::{Commands, DespawnRecursiveExt, Entity, Local, Query, Res, ResMut, With}; + use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; + use kayak_ui::prelude::FontMapping; + + use crate::assets::AssetHandles; + use crate::system::flow::AppState; + use crate::ui::components::cursor_animation_system; + use crate::ui::utilities::StateUIRoot; + // use crate::ui::utilities::App; + + pub fn configure_kayak_ui( + mut commands: Commands, + assets: Res<AssetHandles>, + mut font_mapping: ResMut<FontMapping>, + ) { + log::info!("Configuring Fonts And UI"); + font_mapping.set_default(assets.kayak_font("compass_pro")); + } + + pub fn remove_ui(mut commands: Commands, query: Query<Entity, With<StateUIRoot>>) { + for entity in &query { + commands.entity(entity).despawn_recursive(); + } + } + + pub struct ConfigureKayakPlugin; + impl Plugin for ConfigureKayakPlugin { + fn build(&self, app: &mut App) { + app + // .init_resource::<MenuInterface>() + // .add_system(cursor_animation_system) + .add_exit_system(AppState::Setup, configure_kayak_ui) + // .add_enter_system( + // AppState::Menu, + // super::screens::main_menu::main_menu::render_main_menu, + // ) + // .add_system_set( + // ConditionSet::new() + // .run_in_state(AppState::Menu) + // .with_system(main_menu_router) + // .into(), + // ) + .add_enter_system(AppState::InGame, super::screens::render_in_game_ui) + .add_exit_system(AppState::Menu, remove_ui); + } + } +} + +pub struct AdventUIPlugins; +impl PluginGroup for AdventUIPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::<Self>() + // .add(kayak_ui::bevy::BevyKayakUIPlugin) + .add(_config::ConfigureKayakPlugin) + .add(KayakContextPlugin) + .add(KayakWidgets) + .add(sync::UISyncPlugin) + } +} diff --git a/game_core/src/ui/screens/in_game.rs b/game_core/src/ui/screens/in_game.rs new file mode 100644 index 0000000000000000000000000000000000000000..3638d0e6dc2b00dbd31acfd67368d91ded5fb4ef --- /dev/null +++ b/game_core/src/ui/screens/in_game.rs @@ -0,0 +1,207 @@ +use bevy::prelude::*; +use kayak_ui::prelude::*; +use kayak_ui::widgets::{ElementBundle, KayakAppBundle, TextProps, TextWidgetBundle}; + +use crate::assets::AssetHandles; +use crate::states::Player; +use crate::ui::components::*; +use crate::ui::prelude::{edge_px, pct, px, stretch, value}; +use crate::ui::sync::UITravelInfo; +use crate::ui::utilities::context::create_root_context; +use crate::ui::utilities::{widget_update_with_resource, StateUIRoot}; +use crate::world::{CurrentResidence, MapQuery, TownPaths}; +use crate::{ + empty_props, on_button_click, parent_widget, register_widget_with_resource, + register_widget_with_update, +}; + +empty_props!(InGameProps); +parent_widget!(InGameProps => InGameLayout); + +pub fn transit_button_factory(target: String) -> OnEvent { + let target = target.clone(); + on_button_click!( + ParamSet<( + Commands, + Res<TownPaths>, + Query<(Entity, &CurrentResidence), With<Player>>, + MapQuery, + )>, + |mut params: ParamSet<( + Commands, + Res<TownPaths>, + Query<(Entity, &CurrentResidence), With<Player>>, + MapQuery, + )>| { + let target = target.clone(); + let (entity, current) = { + match params.p2().get_single() { + Ok((entity, current)) => (entity.clone(), (current.get_location()).clone()), + _ => return, + } + }; + + let places = match params.p1().routes.get(¤t) { + Some(places) => places.clone(), + None => return, + }; + + let bundle = match params.p3().get_active_level() { + Some(level) => places.create_route_bundle_for(target, level).unwrap(), + None => return, + }; + + params.p0().entity(entity).insert(bundle); + } + ) +} + +pub fn render_game_panels( + In((widget_context, entity)): In<(KayakWidgetContext, Entity)>, + mut commands: Commands, + ui_data: Res<UITravelInfo>, + assets: Res<AssetHandles>, + places: Res<TownPaths>, +) -> bool { + let parent_id = Some(entity); + + let distance_style = KStyle { + position_type: value(KPositionType::SelfDirected), + top: stretch(1.0), + left: stretch(1.0), + right: stretch(1.0), + bottom: px(50.0), + ..Default::default() + }; + + let panel_style = KStyle { + position_type: value(KPositionType::SelfDirected), + width: pct(65.0), + height: pct(80.0), + min_width: px(400.0), + min_height: px(300.0), + max_width: px(850.0), + max_height: px(550.0), + top: stretch(1.0), + left: stretch(1.0), + right: stretch(1.0), + bottom: stretch(1.0), + layout_type: value(LayoutType::Column), + row_between: px(20.0), + ..Default::default() + }; + + rsx! { + <ElementBundle + styles={KStyle { + ..Default::default() + }} + > + { if ui_data.distance_remaining > 0.1 { + constructor! { + <TextWidgetBundle + text={TextProps { + content: format!("{:.2}KM", ui_data.distance_remaining), + size: 48.0, + ..Default::default() + }} + styles={distance_style} + /> + } + }} + + {if ui_data.is_in_town { + if let Some(ref place) = ui_data.current_town { + constructor! { + <PanelWidget + styles={panel_style} + > + <TextWidgetBundle + text={TextProps { + content: format!("Hark! You enter {}", &place), + size: 48.0, + ..Default::default() + }} + styles={KStyle { + color: value(Color::BLACK), + padding: edge_px(20.0), + left: stretch(1.0), + right: stretch(1.0), + ..Default::default() + }} + /> + + <VDividerWidget props={VDividerWidgetProps { height: 4.0, padding: 5.0, color: Color::rgb(0.52, 0.369, 0.18)}} /> + + <TextWidgetBundle + text={TextProps { + content: format!("Set off for:"), + size: 32.0, + ..Default::default() + }} + styles={KStyle { + color: value(Color::BLACK), + padding: edge_px(20.0), + left: stretch(1.0), + right: stretch(1.0), + ..Default::default() + }} + /> + + { + for (place, distance) in ui_data.travel_options.iter() { + constructor! { + <ButtonWidget + styles={ + KStyle { + left: stretch(1.0), + right: stretch(1.0), + width: pct(70.0), + min_width: px(300.0), + max_width: px(600.0), + bottom: px(10.0), + ..Default::default() + } + } + props={ButtonWidgetProps::text(format!("{}: {:.2}KM", &place, distance), 28.0)} + on_event={transit_button_factory(place.clone())} + /> + } + } + } + </PanelWidget> + } + } + }} + </ElementBundle> + } + + true +} + +pub fn render_in_game_ui(mut commands: Commands) { + let parent_id = None; + let mut widget_context = create_ingame_context(); + + rsx! { + <KayakAppBundle> + <InGameLayout /> + </KayakAppBundle> + } + + commands.spawn((UICameraBundle::new(widget_context), StateUIRoot)); +} + +fn create_ingame_context() -> KayakRootContext { + let mut widget_context = create_root_context(); + + register_widget_with_resource!( + widget_context, + InGameProps, + EmptyState, + UITravelInfo, + render_game_panels + ); + + widget_context +} diff --git a/game_core/src/ui/screens/mod.rs b/game_core/src/ui/screens/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..2cce8e4c4109137b7a1ea0cfe3698bae1fa1fc9b --- /dev/null +++ b/game_core/src/ui/screens/mod.rs @@ -0,0 +1,3 @@ +mod in_game; + +pub use in_game::render_in_game_ui; diff --git a/game_core/src/ui/sync/mod.rs b/game_core/src/ui/sync/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d6358a72b17dccf006a1c82ee9659129e3a7daa1 --- /dev/null +++ b/game_core/src/ui/sync/mod.rs @@ -0,0 +1,26 @@ +use bevy::app::{App, Plugin}; +use iyes_loopless::prelude::ConditionSet; + +use crate::system::flow::AppState; + +mod sync_travel; + +pub struct UISyncPlugin; +impl Plugin for UISyncPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(sync_travel::UITravelInfo { + travel_options: Vec::new(), + is_in_town: false, + current_town: None, + distance_remaining: 0.0, + }) + .add_system_set( + ConditionSet::new() + .run_in_state(AppState::InGame) + .with_system(sync_travel::sync_player_travel_info_to_ui) + .into(), + ); + } +} + +pub use sync_travel::UITravelInfo; diff --git a/game_core/src/ui/sync/sync_travel.rs b/game_core/src/ui/sync/sync_travel.rs new file mode 100644 index 0000000000000000000000000000000000000000..585f492b45f649e764e5788250be8035e26c32d5 --- /dev/null +++ b/game_core/src/ui/sync/sync_travel.rs @@ -0,0 +1,52 @@ +use bevy::math::Vec3Swizzles; +use bevy::prelude::*; + +use crate::states::Player; +use crate::world::{CurrentResidence, MapQuery, TownPaths, TravelPath}; + +#[derive(Resource, Clone)] +pub struct UITravelInfo { + pub distance_remaining: f32, + pub is_in_town: bool, + pub travel_options: Vec<(String, f32)>, + pub current_town: Option<String>, +} + +pub fn sync_player_travel_info_to_ui( + player_query: Query<(&Transform, Option<&TravelPath>, &CurrentResidence), With<Player>>, + mut ui_info: ResMut<UITravelInfo>, + places: Res<TownPaths>, + map_query: MapQuery, +) { + for (transform, maybe_path, residence) in &player_query { + if let Some(path) = maybe_path { + let remaining_distance = path.ui_distance_remaining(); + let current_edge_length = path.get_current_edge_distance(); + let distance_to_next = path.distance_to_next_node(transform.translation.xy()); + + ui_info.distance_remaining = + remaining_distance - (current_edge_length - distance_to_next); + } + match residence { + CurrentResidence::TravellingFrom(place) => { + if ui_info.is_in_town { + ui_info.current_town = None; + ui_info.is_in_town = false; + } + } + CurrentResidence::RestingAt(place) => { + if !ui_info.is_in_town { + ui_info.is_in_town = true; + ui_info.current_town = Some(place.clone()); + let level = map_query.get_active_level().unwrap(); + let destinations = places.routes.get(place).unwrap(); + ui_info.travel_options = destinations + .routes + .iter() + .map(|(loc, route)| (loc.clone(), route.calculate_distance(level))) + .collect(); + } + } + } + } +} diff --git a/game_core/src/ui/utilities.rs b/game_core/src/ui/utilities.rs new file mode 100644 index 0000000000000000000000000000000000000000..54c2ca887fef587d356ed7a67a01b8dea5558b26 --- /dev/null +++ b/game_core/src/ui/utilities.rs @@ -0,0 +1,216 @@ +use bevy::prelude::{ + Commands, Component, DespawnRecursiveExt, Entity, In, Query, Res, Resource, With, +}; +use kayak_ui::prelude::{ + Event, EventDispatcherContext, KayakRootContext, KayakWidgetContext, WidgetParam, WidgetState, +}; + +#[derive(bevy::prelude::Component, Clone, PartialEq, Default)] +pub struct EmptyProps; + +#[macro_export] +macro_rules! empty_props { + ($name: ident) => { + #[derive(::bevy::prelude::Component, Clone, PartialEq, Default)] + #[repr(transparent)] + pub struct $name($crate::ui::utilities::EmptyProps); + impl kayak_ui::prelude::Widget for $name {} + }; +} + +#[macro_export] +macro_rules! basic_widget { + ($props: ident => $name: ident) => { + #[derive(bevy::prelude::Bundle)] + pub struct $name { + pub props: $props, + pub name: ::kayak_ui::prelude::WidgetName, + pub styles: ::kayak_ui::prelude::KStyle, + pub computed_styles: ::kayak_ui::prelude::ComputedStyles, + } + + impl std::default::Default for $name { + fn default() -> $name { + let props = $props::default(); + $name { + name: kayak_ui::prelude::Widget::get_name(&props), + props, + styles: ::kayak_ui::prelude::KStyle::default(), + computed_styles: ::kayak_ui::prelude::ComputedStyles::default(), + } + } + } + }; +} +#[macro_export] +macro_rules! parent_widget { + ($props: ident => $name: ident) => { + #[derive(bevy::prelude::Bundle)] + pub struct $name { + pub props: $props, + pub name: ::kayak_ui::prelude::WidgetName, + pub styles: ::kayak_ui::prelude::KStyle, + pub computed_styles: ::kayak_ui::prelude::ComputedStyles, + pub children: ::kayak_ui::prelude::KChildren, + pub on_event: ::kayak_ui::prelude::OnEvent, + } + + impl std::default::Default for $name { + fn default() -> $name { + let props = $props::default(); + $name { + name: kayak_ui::prelude::Widget::get_name(&props), + props, + styles: ::kayak_ui::prelude::KStyle::default(), + computed_styles: ::kayak_ui::prelude::ComputedStyles::default(), + children: ::kayak_ui::prelude::KChildren::default(), + on_event: ::kayak_ui::prelude::OnEvent::default(), + } + } + } + }; +} + +#[macro_export] +macro_rules! register_widget { + ($ctx: expr, $props: ident, $state: ident, $system: ident) => {{ + $ctx.add_widget_data::<$props, $state>(); + $ctx.add_widget_system( + ::kayak_ui::prelude::Widget::get_name(&$props::default()), + ::kayak_ui::prelude::widget_update::<$props, $state>, + $system, + ); + }}; +} + +#[macro_export] +macro_rules! register_widget_with_update { + ($ctx: expr, $props: ident, $state: ident, $system: ident, $update: expr) => {{ + $ctx.add_widget_data::<$props, $state>(); + $ctx.add_widget_system( + ::kayak_ui::prelude::Widget::get_name(&$props::default()), + $update, + $system, + ); + }}; +} + +#[macro_export] +macro_rules! register_widget_with_resource { + ($ctx: expr, $props: ident, $state: ident, $resource: ident, $system: ident) => {{ + $ctx.add_widget_data::<$props, $state>(); + $ctx.add_widget_system( + ::kayak_ui::prelude::Widget::get_name(&$props::default()), + $crate::ui::utilities::widget_update_with_resource::<$props, $state, $resource>, + $system, + ); + }}; +} + +pub fn widget_update_with_resource< + Props: PartialEq + Component + Clone, + State: PartialEq + Component + Clone, + External: Resource, +>( + In((widget_context, entity, previous_entity)): In<(KayakWidgetContext, Entity, Entity)>, + resource: Res<External>, + widget_param: WidgetParam<Props, State>, +) -> bool { + widget_param.has_changed(&widget_context, entity, previous_entity) || resource.is_changed() +} + +#[derive(Component)] +pub struct StateUIRoot; + +fn remove_root_ui( + mut commands: Commands, + query: Query<Entity, (With<KayakRootContext>, With<StateUIRoot>)>, +) { + for entity in &query { + commands.entity(entity).despawn_recursive(); + } +} + +pub mod context { + use kayak_ui::prelude::{widget_update, EmptyState, KayakRootContext}; + use kayak_ui::widgets::KayakWidgetsContextPlugin; + use kayak_ui::KayakUIPlugin; + + use crate::register_widget; + use crate::ui::components::*; + + pub fn create_root_context() -> KayakRootContext { + let mut widget_context = KayakRootContext::new(); + widget_context.add_plugin(KayakWidgetsContextPlugin); + widget_context.add_plugin(AdventWidgetsPlugin); + widget_context + } + + pub struct AdventWidgetsPlugin; + impl KayakUIPlugin for AdventWidgetsPlugin { + fn build(&self, widget_context: &mut KayakRootContext) { + register_widget!( + widget_context, + DebugInfoProps, + EmptyState, + render_debug_info + ); + register_widget!(widget_context, PanelProps, EmptyState, render_panel_widget); + register_widget!( + widget_context, + ButtonWidgetProps, + ButtonWidgetState, + render_button_widget + ); + register_widget!( + widget_context, + ImageButtonWidgetProps, + ImageButtonWidgetState, + render_image_button_widget + ); + register_widget!( + widget_context, + VDividerWidgetProps, + EmptyState, + render_v_divider + ); + register_widget!( + widget_context, + ATextBoxProps, + ATextBoxState, + render_text_box_widget + ); + } + } +} + +pub type OnEventParams = In<(EventDispatcherContext, WidgetState, Event, Entity)>; + +#[macro_export] +macro_rules! on_button_click { + ($param_type: ty, $func_body: expr) => { + ::kayak_ui::prelude::OnEvent::new( + move |::bevy::prelude::In(( + event_dispatcher_context, + _widget_state, + event, + _entity, + )): ::bevy::prelude::In<( + ::kayak_ui::prelude::EventDispatcherContext, + ::kayak_ui::prelude::WidgetState, + ::kayak_ui::prelude::Event, + ::bevy::prelude::Entity, + )>, + params: $param_type| { + match event.event_type { + ::kayak_ui::prelude::EventType::Click(..) => { + $func_body(params); + } + _ => {} + } + + (event_dispatcher_context, event) + }, + ) + }; +} diff --git a/game_core/src/world/debug.rs b/game_core/src/world/debug.rs new file mode 100644 index 0000000000000000000000000000000000000000..97c9eae4e2f8f66a96dea13b9db91374bba8bb0c --- /dev/null +++ b/game_core/src/world/debug.rs @@ -0,0 +1,32 @@ +use bevy::prelude::*; + +use crate::assets::AssetHandles; +use crate::world::TravelPath; + +#[derive(Component)] +pub struct Tombstone; + +pub fn create_tombstones( + mut commands: Commands, + assets: Res<AssetHandles>, + query: Query<&TravelPath, Or<(Added<TravelPath>, Changed<TravelPath>)>>, + existing: Query<Entity, With<Tombstone>>, +) { + for path in &query { + for entity in &existing { + commands.entity(entity).despawn_recursive(); + } + + for point in &path.path { + commands.spawn(( + SpriteSheetBundle { + transform: Transform::from_translation(point.extend(900.0)), + texture_atlas: assets.atlas("characters"), + sprite: TextureAtlasSprite::new(1), + ..Default::default() + }, + Tombstone, + )); + } + } +} diff --git a/game_core/src/world/debug_gen.rs b/game_core/src/world/generators/debug_gen.rs similarity index 95% rename from game_core/src/world/debug_gen.rs rename to game_core/src/world/generators/debug_gen.rs index d8481accaf111268b0a819c6e1760ce879d457a1..48c2c2b18e303f7b7b84163ec4f35dccd3e695a9 100644 --- a/game_core/src/world/debug_gen.rs +++ b/game_core/src/world/generators/debug_gen.rs @@ -4,7 +4,7 @@ use bevy::render::texture::{BevyDefault, TextureFormatPixelInfo}; use fastrand::Rng; use crate::system::utilities::Indexer; -use crate::world::generate_overworld::{generate_overworld, WorldTileType}; +use crate::world::generators::generate_overworld::{generate_overworld, WorldTileType}; pub fn generate_and_spawn_world(mut commands: Commands, mut images: ResMut<Assets<Image>>) { let world = generate_overworld(300, 300, Rng::new()); diff --git a/game_core/src/world/generate_overworld.rs b/game_core/src/world/generators/generate_overworld.rs similarity index 100% rename from game_core/src/world/generate_overworld.rs rename to game_core/src/world/generators/generate_overworld.rs diff --git a/game_core/src/world/handle_overworld.rs b/game_core/src/world/generators/handle_overworld.rs similarity index 82% rename from game_core/src/world/handle_overworld.rs rename to game_core/src/world/generators/handle_overworld.rs index 27f432d0158c37838bad6257ac22030863aead92..70837fed8cc83d9b54d4ed1d7b5f609997c05c2a 100644 --- a/game_core/src/world/handle_overworld.rs +++ b/game_core/src/world/generators/handle_overworld.rs @@ -9,19 +9,27 @@ use fastrand::Rng; use crate::assets::AssetHandles; use crate::system::camera::GameCamera; use crate::system::utilities::Indexer; -use crate::world::generate_overworld::{generate_overworld, Overworld}; +use crate::world::generators::generate_overworld::{generate_overworld, Overworld}; +use crate::world::generators::{ParticleRollingGenerator, WorldGenerator}; pub fn spawn_new_map( mut commands: Commands, assets: Res<AssetHandles>, mut query: Query<&mut Transform, With<GameCamera>>, ) { - let world = generate_overworld(300, 300, Rng::new()); + let indexer = Indexer::new(500, 500); + let rng = Rng::new(); + + let world = Overworld { + width: 500, + height: 500, + terrain: ParticleRollingGenerator::new(21000, 400).generate(&indexer, &rng), + }; + spawn_overworld(&world, &mut commands, &assets); for mut camera_transform in &mut query { - log::info!("SET CAM TRANSFORM"); camera_transform.translation = - vec3(150.0 * 4.0, 150.0 * 4.0, camera_transform.translation.z); + vec3(250.0 * 4.0, 250.0 * 4.0, camera_transform.translation.z); } } diff --git a/game_core/src/world/generators/mod.rs b/game_core/src/world/generators/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..d012fc5280727e9761d0f73dbef598bf27c4de6d --- /dev/null +++ b/game_core/src/world/generators/mod.rs @@ -0,0 +1,30 @@ +use fastrand::Rng; +use num_traits::AsPrimitive; + +use crate::system::utilities::Indexer; +use crate::world::generators::generate_overworld::WorldTileType; + +pub mod debug_gen; +mod generate_overworld; +pub mod handle_overworld; +mod particle_rolling; + +pub trait WorldGenerator { + fn generate(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType>; + fn map_noise_value(noise: impl AsPrimitive<f64>) -> WorldTileType { + let noise = noise.as_(); + if noise < 0.1 { + WorldTileType::Water + } else if noise < 0.2 { + WorldTileType::Sand + } else if noise < 0.6 { + WorldTileType::Grass + } else if noise < 0.8 { + WorldTileType::Snow + } else { + WorldTileType::Mountain + } + } +} + +pub use self::particle_rolling::ParticleRollingGenerator; diff --git a/game_core/src/world/generators/particle_rolling.rs b/game_core/src/world/generators/particle_rolling.rs new file mode 100644 index 0000000000000000000000000000000000000000..d43a13411b2f5d8787631f8c595a9d488a1d0649 --- /dev/null +++ b/game_core/src/world/generators/particle_rolling.rs @@ -0,0 +1,118 @@ +use std::iter::zip; + +use bevy::prelude::UVec2; +use fastrand::Rng; +use noise::{NoiseFn, Perlin}; + +use crate::system::utilities::Indexer; +use crate::world::generators::generate_overworld::WorldTileType; +use crate::world::generators::WorldGenerator; + +#[derive(Copy, Clone, Debug)] +pub struct ParticleRollingGenerator { + particle_iterations: usize, + particle_lifespan: usize, +} + +impl ParticleRollingGenerator { + pub fn new(iterations: usize, lifespan: usize) -> Self { + Self { + particle_iterations: iterations, + particle_lifespan: lifespan, + } + } +} + +impl Default for ParticleRollingGenerator { + fn default() -> Self { + ParticleRollingGenerator::new(5000, 50) + } +} + +impl WorldGenerator for ParticleRollingGenerator { + fn generate(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType> { + let mut map = self.create_map(indexer, rng); + let mut attempts = 500; + + while attempts > 0 { + let grass_count = map.iter().fold(0, |count, tile| { + if *tile == WorldTileType::Grass { + count + 1 + } else { + count + } + }); + + let water_count = map.iter().fold(0, |count, tile| { + if *tile == WorldTileType::Water { + count + 1 + } else { + count + } + }); + + if grass_count > (map.len() - water_count) / 2 { + log::info!("Got good map"); + break; + } + + log::info!("Doing a new map now; remaining {}", attempts); + map = self.create_map(indexer, rng); + attempts -= 1; + } + + map + } +} + +impl ParticleRollingGenerator { + fn create_map(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType> { + let width = indexer.width(); + let height = indexer.height(); + let mut values = vec![0.0; width * height]; + let mut mask = vec![0.0; width * height]; + + for _ in 0..self.particle_iterations { + let mut lifespan = self.particle_lifespan; + let mut particle_x = rng.usize(width / 6..width - width / 6); + let mut particle_y = rng.usize(height / 6..height - height / 6); + + while lifespan > 0 { + let idx = indexer.index(particle_x, particle_y); + let new_value = mask[idx] + 1.0 / 100.0; + + mask[idx] = new_value; + let choices: Vec<UVec2> = indexer + .square_adjacent(particle_x, particle_y) + .iter() + .filter(|UVec2 { x, y }| mask[indexer.index(*x, *y)] <= new_value) + .copied() + .collect(); + + if !choices.is_empty() { + let next = choices[rng.usize(0..choices.len())]; + particle_x = next.x as usize; + particle_y = next.y as usize; + } + + lifespan -= 1; + } + } + + // return mask.iter().copied().map(Self::map_noise_value).collect(); + + let noise = Perlin::new(rng.u32(0..u32::MAX)); + let values: Vec<f64> = values + .iter() + .enumerate() + .map(|(index, _)| { + let (x, y) = indexer.reverse(index); + noise.get([x as f64 / width as f64, y as f64 / height as f64]) + }) + .collect(); + + zip(values, mask) + .map(|(left, right)| Self::map_noise_value(left * right)) + .collect() + } +} diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs index f9931258f6b783ec6d1ac18f0ba83f0d0387e288..2e9349e79aeaf894cb52586ca312bf4f993fd678 100644 --- a/game_core/src/world/mod.rs +++ b/game_core/src/world/mod.rs @@ -1,18 +1,35 @@ use bevy::app::App; -use bevy::prelude::Plugin; -use fastrand::Rng; -use iyes_loopless::prelude::AppLooplessStateExt; +use bevy::prelude::{Commands, Plugin}; +use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; use crate::system::flow::AppState; -use crate::world::debug_gen::generate_and_spawn_world; +use crate::world::utils::ActiveLevel; -mod debug_gen; -mod generate_overworld; -mod handle_overworld; +mod debug; +mod generators; +mod spawning; +mod towns; +mod travel; +mod utils; +mod world_query; pub struct WorldPlugin; impl Plugin for WorldPlugin { fn build(&self, app: &mut App) { - app.add_enter_system(AppState::InGame, handle_overworld::spawn_new_map); + app.init_resource::<TownPaths>() + .add_enter_system(AppState::InGame, |mut commands: Commands| { + commands.insert_resource(ActiveLevel::new("Grantswaith")); + }) + .add_system_set( + ConditionSet::new() + .run_in_state(AppState::InGame) + // .with_system(debug::create_tombstones) + .with_system(spawning::spawn_world_data) + .with_system(travel::tick_travelling_merchant) + .into(), + ); } } + +pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget}; +pub use world_query::{CameraBounds, MapQuery}; diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c94ea2b1b70368c270fd65b974b990b897e6dff --- /dev/null +++ b/game_core/src/world/spawning.rs @@ -0,0 +1,157 @@ +use bevy::prelude::*; +use bevy_ecs_tilemap::prelude::*; +use num_traits::AsPrimitive; + +use crate::assets::{AssetHandles, LdtkProject, LevelIndex}; +use crate::states::Player; +use crate::system::camera::ChaseCam; +use crate::world::towns::{CurrentResidence, TownPaths}; +use crate::world::utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32}; +use crate::world::world_query::MapQuery; + +pub fn spawn_world_data( + mut commands: Commands, + mut active_level: Option<ResMut<ActiveLevel>>, + assets: Res<AssetHandles>, + projects: Res<Assets<LdtkProject>>, + level_index: Res<LevelIndex>, + mut last_spawned_level: Local<String>, +) { + let mut active_level = match active_level { + Some(l) => l, + None => return, + }; + + if *last_spawned_level == active_level.map && !active_level.dirty { + return; + } else { + *last_spawned_level = active_level.map.clone(); + } + + if active_level.is_changed() || active_level.is_added() { + let level = match level_index.get(&active_level.map) { + Some(l) => l, + _ => return, + }; + + let map_tile_width = px_to_grid(level.px_wid); + let map_tile_height = px_to_grid(level.px_hei); + + let tilemap_size = TilemapSize { + x: map_tile_width.as_(), + y: map_tile_height.as_(), + }; + let tilemap_tile_size = TilemapTileSize { + x: TILE_SCALE_F32, + y: TILE_SCALE_F32, + }; + + MapQuery::for_each_layer_of(level, |layer| { + if !layer.has_tiles() { + return; + } + + let map_entity = commands.spawn_empty().id(); + let mut storage = TileStorage::empty(tilemap_size); + + layer.for_each_tile(|x, y, tile| { + let mut tile_pos = TilePos { + x: px_to_grid(x).as_(), + y: px_to_grid(y).as_(), + }; + + tile_pos.y = map_tile_height as u32 - tile_pos.y - 1; + + let tile_entity = commands + .spawn(TileBundle { + position: tile_pos, + tilemap_id: TilemapId(map_entity), + texture_index: TileTextureIndex(tile.tile.t as u32), + ..Default::default() + }) + .id(); + + storage.set(&tile_pos, tile_entity); + }); + + let grid_size = tilemap_tile_size.into(); + let map_type = TilemapType::Square; + + log::info!("Spawning tilemap"); + + let bg = level + .level_bg_color + .clone() + .unwrap_or_else(|| level.bg_color.clone()); + + match bg.len() { + 6 => commands.insert_resource(ClearColor( + Color::hex(bg.clone()).expect("Failed to set background color"), + )), + 7 => commands.insert_resource(ClearColor( + Color::hex(&bg[1..]).expect("Failed to set background color"), + )), + _ => { + log::warn!("Strange colour {}", &bg) + } + } + + commands.entity(map_entity).insert(( + TilemapBundle { + grid_size, + map_type, + size: tilemap_size, + storage, + texture: TilemapTexture::Single(assets.image("overworld")), + tile_size: tilemap_tile_size, + transform: Transform::from_xyz(0.0, 0.0, 5.0 + layer.get_z_delta()), // get_tilemap_center_transform(&tilemap_size, &grid_size, 5.0), + ..Default::default() + }, + WorldLinked, + )); + }); + + let trade_routes = TownPaths::from(MapQuery::get_entities_of(level)); + let random_start = trade_routes + .routes + .values() + .nth(fastrand::usize(0..trade_routes.routes.len())); + + if let Some(start) = random_start { + if let Some(route) = start.routes.values().next() { + let point = route.nodes[0]; + + commands.spawn(( + SpriteSheetBundle { + transform: Transform::from_xyz( + grid_to_px(point.tile_x), + level.px_hei as f32 - grid_to_px(point.tile_y), + 400.0, + ), + texture_atlas: assets.atlas("characters"), + sprite: TextureAtlasSprite { + index: 0, + ..Default::default() + }, + ..Default::default() + }, + start + .create_route_bundle_for( + start + .routes + .keys() + .nth(fastrand::usize(0..start.routes.len())) + .cloned() + .unwrap(), + level, + ) + .unwrap(), + Player, + ChaseCam, + )); + } + } + + commands.insert_resource(trade_routes); + } +} diff --git a/game_core/src/world/towns.rs b/game_core/src/world/towns.rs new file mode 100644 index 0000000000000000000000000000000000000000..67705f35b18fceac8cce7d5a81860f15df67a5f7 --- /dev/null +++ b/game_core/src/world/towns.rs @@ -0,0 +1,247 @@ +use bevy::math::{vec2, Vec2}; +use bevy::prelude::{Bundle, Component, Resource}; +use bevy::utils::HashMap; +use ldtk_rust::{EntityInstance, Level}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::world::utils::{grid_to_px, px_to_grid}; + +pub type Town = String; + +/// Stores the ID of the most recent town that has been inhabited, +/// as well as the current travel state +#[derive(Component)] +pub enum CurrentResidence { + TravellingFrom(String), + RestingAt(String), +} + +impl CurrentResidence { + pub fn is_travelling(&self) -> bool { + match self { + CurrentResidence::TravellingFrom(..) => true, + _ => false, + } + } + pub fn get_location(&self) -> &String { + match self { + CurrentResidence::TravellingFrom(place) | CurrentResidence::RestingAt(place) => place, + } + } +} + +#[derive(Component)] +pub struct TravelTarget(pub String); + +#[derive(Component)] +pub struct TravelPath { + pub path: Vec<Vec2>, + last_index: usize, + next_index: usize, +} + +pub enum PathingResult { + Travelling, + NextNode, + PathComplete, +} + +fn calculate_distance(points: &mut impl Iterator<Item = Vec2>) -> f32 { + let mut total = 0.0; + let mut previous = points.next().unwrap(); + while let Some(point) = points.next() { + total += previous.distance(point); + previous = point; + } + total +} + +impl TravelPath { + pub fn increment_indexes(&mut self) { + self.last_index += 1; + self.next_index += 1; + } + + pub fn get_step(&self, current: Vec2, speed: f32) -> Vec2 { + if let Some(next) = self.path.get(self.next_index) { + let step = current - *next; + step.normalize_or_zero() * speed + } else { + Vec2::ZERO + } + } + pub fn get_route_time(&self, speed: f32) -> f32 { + if let (Some(last), Some(next)) = ( + self.path.get(self.last_index), + self.path.get(self.next_index), + ) { + next.distance(*last) / speed + } else { + 0.0 + } + } + + pub fn check_position(&mut self, current: Vec2) -> PathingResult { + if let Some(next) = self.path.get(self.next_index) { + if current.distance(*next).abs() < 0.05 { + PathingResult::NextNode + } else { + PathingResult::Travelling + } + } else { + PathingResult::PathComplete + } + } + + pub fn ui_distance_remaining(&self) -> f32 { + calculate_distance(&mut self.path.iter().skip(self.last_index).copied()) + } + + pub fn distance_to_next_node(&self, other: Vec2) -> f32 { + if let Some(next) = self.path.get(self.next_index) { + other.distance(*next).abs() + } else { + 0.0 + } + } + + pub fn get_current_edge_distance(&self) -> f32 { + if let (Some(last), Some(next)) = ( + self.path.get(self.last_index), + self.path.get(self.next_index), + ) { + last.distance(*next).abs() + } else { + 0.0 + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Debug)] +pub struct RouteNode { + #[serde(alias = "cx")] + pub tile_x: usize, + #[serde(alias = "cy")] + pub tile_y: usize, +} + +#[derive(Clone, Eq, Ord, PartialOrd, PartialEq, Debug)] +pub struct Route { + pub nodes: Vec<RouteNode>, +} + +impl Route { + pub fn create_travel_path_for(&self, level: &Level) -> TravelPath { + TravelPath { + path: self + .nodes + .iter() + .map(|node| { + vec2( + grid_to_px(node.tile_x), + level.px_hei as f32 - grid_to_px(node.tile_y), + ) + }) + .collect(), + last_index: 0, + next_index: 1, + } + } + + pub fn calculate_distance(&self, level: &Level) -> f32 { + calculate_distance(&mut self.nodes.iter().map(|node| { + vec2( + grid_to_px(node.tile_x), + level.px_hei as f32 - grid_to_px(node.tile_y), + ) + })) + } +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Destinations { + pub source: Town, + pub routes: HashMap<String, Route>, +} + +impl Destinations { + pub fn create_route_bundle_for(&self, target: String, level: &Level) -> Option<impl Bundle> { + self.routes.get(&target).map(|route| { + ( + route.create_travel_path_for(level), + CurrentResidence::TravellingFrom(self.source.clone()), + TravelTarget(target.clone()), + ) + }) + } +} + +#[derive(Clone, Eq, PartialEq, Debug, Default, Resource)] +pub struct TownPaths { + pub routes: HashMap<Town, Destinations>, +} + +impl From<Vec<&EntityInstance>> for TownPaths { + fn from(value: Vec<&EntityInstance>) -> Self { + let mut routes = HashMap::with_capacity(value.len() * 2); + + 'process_entity: for entity in value { + if entity.identifier != String::from("TradeRoute") { + continue; + } + + let mut from_place = None; + let mut to_place = None; + let mut nodes: Vec<RouteNode> = Vec::new(); + + for field in &entity.field_instances { + match &*field.identifier { + "from" => match &field.value { + Some(Value::String(string)) => from_place = Some((*string).clone()), + _ => continue 'process_entity, + }, + "to" => match &field.value { + Some(Value::String(string)) => to_place = Some((*string).clone()), + _ => continue 'process_entity, + }, + "nodes" => { + if let Some(field_value) = &field.value { + nodes = match serde_json::from_value(field_value.clone()) { + Ok(val) => val, + _ => continue 'process_entity, + }; + } + } + _ => {} + } + } + + if let (Some(from), Some(to)) = (from_place, to_place) { + let mut route_entry = routes.entry(from.clone()).or_insert_with(|| Destinations { + source: from.clone(), + routes: Default::default(), + }); + route_entry.routes.insert( + to.clone(), + Route { + nodes: nodes.clone(), + }, + ); + + let mut route_entry = routes.entry(to.clone()).or_insert_with(|| Destinations { + source: to.clone(), + routes: Default::default(), + }); + route_entry.routes.insert( + from.clone(), + Route { + nodes: nodes.iter().rev().cloned().collect(), + }, + ); + } + } + + TownPaths { routes } + } +} diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs new file mode 100644 index 0000000000000000000000000000000000000000..157bcda0d3d780466204adefcacade95635fca50 --- /dev/null +++ b/game_core/src/world/travel.rs @@ -0,0 +1,38 @@ +use std::ops::SubAssign; + +use bevy::math::Vec3Swizzles; +use bevy::prelude::*; + +use crate::world::towns::{CurrentResidence, TravelPath, TravelTarget}; +use crate::world::PathingResult; + +pub fn tick_travelling_merchant( + mut commands: Commands, + time: Res<Time>, + mut merchant_query: Query<( + Entity, + &mut Transform, + &mut TravelPath, + &mut TravelTarget, + &mut CurrentResidence, + )>, +) { + let delta = time.delta_seconds(); + for (entity, mut transform, mut path, mut target, mut residence) in &mut merchant_query { + let step = path.get_step(transform.translation.xy(), 16.0) * delta; + transform.translation.sub_assign(step.extend(0.0)); + + match path.check_position(transform.translation.xy()) { + PathingResult::PathComplete => { + commands + .entity(entity) + .remove::<(TravelPath, TravelTarget)>(); + *residence = CurrentResidence::RestingAt(target.0.clone()); + } + PathingResult::NextNode => { + path.increment_indexes(); + } + PathingResult::Travelling => {} + } + } +} diff --git a/game_core/src/world/utils.rs b/game_core/src/world/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..63abfb2914481d6d1c9d2301cb1f3c8fd2272db5 --- /dev/null +++ b/game_core/src/world/utils.rs @@ -0,0 +1,42 @@ +use bevy::prelude::{Component, Resource}; +use ldtk_rust::EntityInstance; +use num_traits::AsPrimitive; + +pub const TILE_SCALE_INT: i64 = 4; +pub const TILE_SCALE_F32: f32 = 4.0; + +pub fn px_to_grid<T: AsPrimitive<i64>>(t: T) -> i64 { + t.as_() / TILE_SCALE_INT +} + +pub fn grid_to_px<T: AsPrimitive<f32>>(t: T) -> f32 { + t.as_() * TILE_SCALE_F32 + (TILE_SCALE_F32 / 2.0) +} + +pub fn entity_to_worldspace(level_height: i64, entity: &EntityInstance) -> (f32, f32) { + let centre_align_pixel_x = grid_to_px(entity.grid[0]) - (TILE_SCALE_F32 / 2.0); + let centre_align_pixel_y = grid_to_px(entity.grid[1]) - (TILE_SCALE_F32 / 2.0); + let inverted_pixel_y = level_height as f32 - centre_align_pixel_y - TILE_SCALE_F32; + let box_aligned_x = centre_align_pixel_x + (entity.width / 2) as f32; + let box_aligned_y = inverted_pixel_y - (entity.height / 2) as f32; + + (box_aligned_x, box_aligned_y) +} + +#[derive(Component)] +pub struct WorldLinked; + +#[derive(Default, Resource, Clone)] +pub struct ActiveLevel { + pub map: String, + pub dirty: bool, +} + +impl ActiveLevel { + pub fn new<T: ToString>(map: T) -> Self { + ActiveLevel { + map: map.to_string(), + dirty: false, + } + } +} diff --git a/game_core/src/world/world_query.rs b/game_core/src/world/world_query.rs new file mode 100644 index 0000000000000000000000000000000000000000..5d646ac4f32bd721980b6e43d945086435feb276 --- /dev/null +++ b/game_core/src/world/world_query.rs @@ -0,0 +1,154 @@ +use std::marker::PhantomData; + +use bevy::ecs::system::SystemParam; +use bevy::prelude::*; +use ldtk_rust::{EntityInstance, LayerInstance, Level, TileInstance}; +use num_traits::AsPrimitive; + +// use crate::assets::level_index::LevelIndex; +use crate::assets::{LdtkProject, LevelIndex}; +use crate::system::utilities::{f32_max, f32_min}; +use crate::world::utils::{ActiveLevel, TILE_SCALE_F32, TILE_SCALE_INT}; + +#[derive(SystemParam)] +pub struct MapQuery<'w, 's> { + assets: Res<'w, Assets<LdtkProject>>, + active: Option<Res<'w, ActiveLevel>>, + index: Res<'w, LevelIndex>, + #[system_param(ignore)] + _e: PhantomData<&'s ()>, +} + +pub struct TileRef<'a> { + pub tile: &'a TileInstance, +} + +impl<'a> TileRef<'a> { + pub fn new(tile: &'a TileInstance) -> Self { + TileRef { tile } + } +} + +impl<'a> TileRef<'a> { + pub fn gid(&self) -> usize { + (self.tile.src[0] * self.tile.src[1]).unsigned_abs() as usize + } +} + +pub struct LayerRef<'a> { + pub idx: usize, + pub layer: &'a LayerInstance, +} + +impl<'a> LayerRef<'a> { + pub fn new(idx: usize, layer: &'a LayerInstance) -> Self { + LayerRef { layer, idx } + } + pub fn has_tiles(&self) -> bool { + !(self.layer.auto_layer_tiles.is_empty() && self.layer.grid_tiles.is_empty()) + } + pub fn for_each_tile(&self, mut cb: impl FnMut(i64, i64, TileRef)) { + self.layer + .grid_tiles + .iter() + .chain(self.layer.auto_layer_tiles.iter()) + .for_each(|tile: &TileInstance| { + let (x, y) = match tile.px.as_slice() { + &[x, y] => (x, y), + _ => { + return; + } + }; + + cb(x, y, TileRef::new(tile)); + }); + } + pub fn get_z_delta(&self) -> f32 { + (self.idx as f32) / 100.0 + } + + // pub fn get_tile_metadata_of(&self, tile_id: i64) { + // self.layer.til + // } +} + +#[derive(Copy, Clone, Debug)] +pub struct CameraBounds { + pub left: f32, + pub top: f32, + pub bottom: f32, + pub right: f32, +} + +impl CameraBounds { + pub fn get_min_x(&self, camera_width: f32) -> f32 { + self.left + (camera_width / 2.0) - (TILE_SCALE_F32 / 2.0) + } + pub fn get_max_x(&self, camera_width: f32) -> f32 { + self.right - (camera_width / 2.0) - (TILE_SCALE_F32 / 2.0) + } + pub fn get_min_y(&self, camera_height: f32) -> f32 { + self.bottom + (camera_height / 2.0) - (TILE_SCALE_F32 / 2.0) + } + pub fn get_max_y(&self, camera_height: f32) -> f32 { + self.top - (camera_height / 2.0) - (TILE_SCALE_F32 / 2.0) + } +} + +impl<'w, 's> MapQuery<'w, 's> { + // --- We put our logic in static accessors because we might source a level other + // --- than the currently active one. 'active' methods are a convenience to + // --- call the static accessors on whatever the current level is + + pub fn for_each_layer_of(level: &Level, mut cb: impl FnMut(LayerRef)) { + if let Some(layers) = level.layer_instances.as_ref() { + for layer in layers.iter().rev().enumerate() { + cb(LayerRef::new(layer.0, layer.1)); + } + } + } + + pub fn get_entities_of(level: &Level) -> Vec<&EntityInstance> { + level + .layer_instances + .as_ref() + .map(|layers| { + layers + .iter() + .flat_map(|layer| layer.entity_instances.iter()) + .collect() + }) + .unwrap_or_default() + } + + pub fn get_camera_bounds_of(level: &Level) -> CameraBounds { + CameraBounds { + left: 0.0, + top: level.px_hei as f32, + bottom: 0.0, + right: level.px_wid as f32, + } + } + + pub fn get_active_level(&self) -> Option<&Level> { + self.active + .as_ref() + .and_then(|index| self.index.get(&index.map)) + } + + pub fn get_entities(&self) -> Vec<&EntityInstance> { + self.get_active_level() + .map(|level| MapQuery::get_entities_of(level)) + .unwrap_or_default() + } + + pub fn get_camera_bounds(&self) -> Option<CameraBounds> { + self.get_active_level().map(MapQuery::get_camera_bounds_of) + } + + pub fn for_each_layer(&self, mut cb: impl FnMut(LayerRef)) { + if let Some(level) = self.get_active_level() { + Self::for_each_layer_of(level, cb); + } + } +} diff --git a/micro_asset_io/src/apack_loader.rs b/micro_asset_io/src/apack_loader.rs index 2417fc45c3e15d9a4b579f81394589b2cab170ef..ecc98bc727bfc00fb03c176b7c84c16880066dff 100644 --- a/micro_asset_io/src/apack_loader.rs +++ b/micro_asset_io/src/apack_loader.rs @@ -21,6 +21,7 @@ pub struct APackLoader; #[uuid = "bce5dd7a-726c-11ed-b53f-071b386b4896"] pub struct APack { pub processed: bool, + pub loaded: bool, pub compressed_bytes: Vec<u8>, pub vfs: HashMap<String, Vec<u8>>, } @@ -68,6 +69,7 @@ impl DerefMut for ClonableAPack { impl Clone for ClonableAPack { fn clone(&self) -> Self { ClonableAPack(APack { + loaded: self.loaded, processed: self.processed, compressed_bytes: self.compressed_bytes.clone(), vfs: self.vfs.clone(), @@ -83,6 +85,7 @@ impl Into<APack> for ClonableAPack { impl APack { pub fn new(bytes: Vec<u8>) -> APack { APack { + loaded: false, processed: false, compressed_bytes: bytes, vfs: Default::default(), @@ -97,6 +100,7 @@ impl APack { pub fn clone_ref(apack: &APack) -> APack { APack { + loaded: apack.loaded, processed: apack.processed, compressed_bytes: apack.compressed_bytes.clone(), vfs: apack.vfs.clone(), diff --git a/raw_assets/.gitignore b/raw_assets/.gitignore index fbeaca80e3ce81fa39959a7c162c6f3e2bccd7ff..6d9884b663e9accb8ff0f70cee0dd3736afd8c43 100644 --- a/raw_assets/.gitignore +++ b/raw_assets/.gitignore @@ -1,6 +1,8 @@ backgrounds/ fonts/ sprites/ +ldtk/ +ui/ !.gitignore !manifest.toml \ No newline at end of file diff --git a/raw_assets/manifest.toml b/raw_assets/manifest.toml index c6536724a339df51bbfcb7d77dcea613885082c0..0bd6b567f23769667a42ccee6ee51c1500a6b2a9 100644 --- a/raw_assets/manifest.toml +++ b/raw_assets/manifest.toml @@ -3,11 +3,52 @@ path = "sprites/overworld.png" name = "overworld" tiles = { size = 4, columns = 32, rows = 64 } +[[spritesheets]] +path = "sprites/characters.png" +name = "characters" +tiles = { size = 8, columns = 16, rows = 16 } + [[images]] path = "backgrounds/main_menu.png" name = "menu_background" format = "png" +[[images]] +path = "ui/panel.png" +name = "panel" +format = "png" + +[[images]] +path = "ui/scroll_panel.png" +name = "scroll_panel" +format = "png" + +[[images]] +path = "ui/button_idle.png" +name = "button_idle" +format = "png" + +[[images]] +path = "ui/button_active.png" +name = "button_active" +format = "png" + +[[images]] +path = "ui/button_down.png" +name = "button_down" +format = "png" + +[[images]] +path = "ui/button_disabled.png" +name = "button_disabled" +format = "png" + [[fonts]] -path = "fonts/CompassPro.ttf" -name = "default" \ No newline at end of file +name = "compass_pro" +ttf = "fonts/CompassPro.ttf" +image = "fonts/CompassPro.png" +msdf = "fonts/CompassPro.kayak_font" + +[[ldtk]] +path = "ldtk/overworld_maps.ldtk" +name = "overworld" \ No newline at end of file