├── .gitattributes ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── bench.rs ├── derive ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── src ├── attribute.rs ├── buffer.rs ├── cowstr.rs ├── fraction.rs ├── lib.rs ├── num_traits.rs ├── operations.rs ├── plugin.rs ├── qualifier.rs ├── querier.rs ├── rounding.rs ├── stat.rs ├── stat_map.rs ├── stream.rs └── types │ ├── flags.rs │ ├── float.rs │ ├── int_pct.rs │ ├── int_ratio.rs │ ├── mod.rs │ └── prioritized.rs └── tests ├── asset.rs ├── complex.rs ├── derive.rs ├── distance.rs ├── fraction.rs ├── qualifier.rs └── serde.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | 17 | # Added by cargo 18 | 19 | /target 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["derive"] 3 | 4 | [package] 5 | name = "bevy_stat_query" 6 | version = "0.3.0" 7 | edition = "2021" 8 | 9 | authors = ["Mincong Lu "] 10 | license = "MIT OR Apache-2.0" 11 | 12 | readme = "README.md" 13 | repository = "https://github.com/mintlu8/bevy_stat_query" 14 | description = """ 15 | Blazing fast and versatile RPG stat system for the bevy engine. 16 | """ 17 | keywords = ["bevy", "rpg", "stat"] 18 | 19 | [features] 20 | default = ["derive"] 21 | derive = ["bevy_stat_query_derive"] 22 | 23 | [lib] 24 | doctest = false 25 | 26 | [dependencies] 27 | bevy_ecs = { version = "0.16.0" } 28 | bevy_app = { version = "0.16.0" } 29 | bevy_reflect = { version = "0.16.0" } 30 | serde = { version = "1", features = ["derive"] } 31 | erased-serde = "0.4.3" 32 | ref-cast = "1" 33 | thiserror = "1" 34 | rustc-hash = "2.0.0" 35 | bevy_stat_query_derive = { version = "0.2.0", path = "./derive", optional = true } 36 | scoped-tls-hkt = "0.1.5" 37 | 38 | [dev-dependencies] 39 | bevy = "0.16.0" 40 | bitflags = "2.4.2" 41 | num_enum = "0.7.2" 42 | postcard = { version = "1.0.8", features = ["alloc"], default-features = false } 43 | serde_json = "1.0.114" 44 | strum = { version = "0.26.2", features = ["derive"] } 45 | bevy_serde_lens = { version = "0.6.0" } 46 | criterion = "0.5.1" 47 | 48 | [[bench]] 49 | name = "bench" 50 | harness = false 51 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Mincong Lu 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mincong Lu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy-stat-query 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/bevy_stat_query.svg)](https://crates.io/crates/bevy_stat_query) 4 | [![Docs](https://docs.rs/bevy_stat_query/badge.svg)](https://docs.rs/bevy_stat_query/latest/bevy_stat_query/) 5 | [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://bevyengine.org/learn/book/plugin-development/) 6 | 7 | Versatile RPG stat system for the bevy engine. 8 | 9 | ## Overview 10 | 11 | In order to represent stats, stat buffs and stat queries in an ECS, 12 | `bevy_stat_query` exclusively uses unordered operations to represent 13 | stats, this includes `add`, `multiply`, `min`, `max` and `or`. 14 | 15 | For instance if we want to evaluate a character's strength, 16 | taken into account buffs and debuffs this can look something like this: 17 | 18 | ```rust 19 | clamp((42 + 4 + 7 + (-4)) * 2 * 0.75, 1, 99) 20 | ``` 21 | 22 | Note how the order of evaluation doesn't matter, which fits perfectly into 23 | the "insert component and have effect" usage pattern of the ECS. 24 | 25 | ## Qualified Stats 26 | 27 | We describe each stat as a `Qualifier` and a `Stat`. 28 | `Stat` is a noun like *strength* or *damage* and 29 | `Qualifier` are adjectives that describes 30 | what this `Stat` can be applied to. 31 | 32 | For example in *fire magic damage*, *(fire, magic)* is the `Qualifier`, 33 | *damage* is the `Stat`. 34 | 35 | ## Modifier and Query 36 | 37 | There are actually two types of stats, `modifier` and `query`. 38 | 39 | A `modifier` is something alone the lines of 40 | 41 | ```text 42 | Increase (fire, magic) damage by 5. 43 | ``` 44 | 45 | While a query is 46 | 47 | ```text 48 | This attack does (fire, magic) damage. 49 | ``` 50 | 51 | When querying for *(fire, magic) damage*, all modifiers that boosts 52 | *damage*, *fire damage*, *magic damage* or *(fire, magic) damage* 53 | can apply to this query. 54 | While modifier that boosts a different qualifier *ice damage* or a 55 | different stat *fire defense* does not apply to this query. 56 | 57 | In `bevy_stat_query`, 58 | a modifier is represented as `(Qualifier, Stat, Value)` while a 59 | query is represented as `(QualifierQuery, Stat)`. 60 | 61 | * Conditional Modifiers 62 | 63 | A common trope in fantasy games is the modifier `elemental damage`, which applies to 64 | any of fire, ice, etc. In `Qualifier` this is the `any_of` field. 65 | 66 | * Exact Query 67 | 68 | Imagine we have an effect like this: 69 | 70 | ```text 71 | Add 50% of the character's magic damage to physical damage. 72 | ``` 73 | 74 | In order to avoid duplication, since effects boosting `damage` applies to 75 | both, we can use `QualifierQuery::exact`. 76 | 77 | ## Traits 78 | 79 | Qualifier is usually a bitflags implementing `QualifierFlag`, Stat is usually an enum deriving `Stat`. 80 | 81 | An app usually has a single `QualifierFlag` but multiple `Stat` implementors, 82 | since each `Stat` can associate to a different type. 83 | For example `strength` and `magic` can be a `i32`, 84 | `hp` can be a `f32`, `is_dragon` can be a `bool` etc. 85 | 86 | Different types of stats can still query each other via `Querier` 87 | to model effects like 88 | 89 | ```text 90 | If user is a dragon, increase damage by 50%. 91 | ``` 92 | 93 | ## `StatStream` and `QueryStream` 94 | 95 | In order for components to contribute to stats, you must implement `QueryStream`. `StatStream` can be used 96 | if no additional querying is needed. A `Component` that implements `StatStream` is automatically a `QueryStream`. 97 | 98 | In order to use `QueryStream`, mark queryable entities as `StatEntity`. 99 | Then add `StatEntities` to you system and join it with various `QueryStream`s. 100 | 101 | ```rust 102 | fn stat_query( 103 | entities: StatEntities, 104 | stat_maps: StatQuery, 105 | weapons: StatQuery, 106 | buffs: ChildQuery, 107 | ) { 108 | let querier = entities.join(&stat_maps).join(&weapons).join(&buffs); 109 | let damage = querier.eval_stat(&MyQualifier::Magic, &MyStat::Damage).unwrap(); 110 | } 111 | ``` 112 | 113 | Using `bevy_stat_query` is significantly easier if you have access to `&mut World`. 114 | One-shot systems are recommended to perform queries. 115 | 116 | ## Relations 117 | 118 | `StatStream` and `QueryStream` provides the `stream_relation` function that makes it easier to implement 119 | relation based effects like 120 | 121 | ```text 122 | Increase damage of all allies within 3 yards by 5. 123 | ``` 124 | 125 | Checkout one of our examples on how to implement this. 126 | 127 | ## Versions 128 | 129 | | bevy | bevy-stat-query | 130 | |------|-----------------| 131 | | 0.15 | 0.1 - latest | 132 | 133 | ## License 134 | 135 | Licensed under either of 136 | 137 | * Apache License, Version 2.0 (LICENSE-APACHE(LICENSE-APACHE) or ) 138 | * MIT license (LICENSE-MIT(LICENSE-MIT) or ) 139 | 140 | at your option. 141 | 142 | ### Contribution 143 | 144 | Unless you explicitly state otherwise, any contribution intentionally submitted 145 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 146 | additional terms or conditions. 147 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, collections::BTreeMap}; 2 | 3 | use bevy_stat_query::{ 4 | operations::StatOperation::Add, types::StatIntPercentAdditive, Qualifier, QualifierQuery, Stat, 5 | StatMap, StatValue, 6 | }; 7 | use criterion::{criterion_group, criterion_main, Criterion}; 8 | 9 | #[derive(Debug, Clone, Copy, Stat)] 10 | #[stat(value = "StatIntPercentAdditive")] 11 | pub struct S; 12 | 13 | pub fn query_many(c: &mut Criterion) { 14 | let mut m = StatMap::::new(); 15 | let mut bt_dyn = BTreeMap::new(); 16 | 17 | for i in 0..1024 { 18 | m.insert_base(Qualifier::all_of(i), S, 1); 19 | bt_dyn.insert(Qualifier::all_of(i), Box::new(1) as Box); 20 | } 21 | 22 | c.bench_function("naive_btree_aggregate_many", |b| { 23 | b.iter(|| { 24 | let mut result = StatIntPercentAdditive::::default(); 25 | bt_dyn 26 | .iter() 27 | .filter(|(q, _)| q.qualifies_as(&QualifierQuery::Aggregate(255))) 28 | .map(|(_, v)| v.downcast_ref::().copied().unwrap()) 29 | .for_each(|v| result.join(Add(v).into_stat())); 30 | result 31 | }) 32 | }); 33 | 34 | c.bench_function("stat_map_aggregate_many", |b| { 35 | b.iter(|| m.eval_stat(&QualifierQuery::Aggregate(255), &S)) 36 | }); 37 | } 38 | 39 | criterion_group!(benches, query_many); 40 | criterion_main!(benches); 41 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_stat_query_derive" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | authors = ["Mincong Lu "] 7 | license = "MIT OR Apache-2.0" 8 | 9 | readme = "README.md" 10 | repository = "https://github.com/mintlu8/bevy_stat_query" 11 | description = """ 12 | Derive macros for `bevy_stat_query`. 13 | """ 14 | keywords = ["bevy", "rpg", "stat"] 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [dependencies] 20 | proc-macro-error = "1.0.4" 21 | proc-macro2 = "1.0.86" 22 | quote = "1.0.36" 23 | syn = "2.0.72" 24 | -------------------------------------------------------------------------------- /derive/README.md: -------------------------------------------------------------------------------- 1 | # bevy-stat-query-derive 2 | 3 | Derive macros for `bevy_stat_query`. 4 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::{Span, TokenStream as TokenStream1}; 2 | use proc_macro_error::{abort, proc_macro_error}; 3 | use quote::{quote, ToTokens}; 4 | use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Fields, LitInt, LitStr, Type}; 5 | 6 | /// Derive macro for `Stat`. 7 | /// 8 | /// # Syntax 9 | /// 10 | /// The macro works for unit structs and fieldless enums 11 | /// with unsigned `repr`. 12 | /// 13 | /// ``` 14 | /// #[derive(Debug, Clone, Copy, Stat)] 15 | /// #[stat(value = "StatIntPercentAdditive")] 16 | /// pub struct MyStat; 17 | /// ``` 18 | /// 19 | /// or 20 | /// 21 | /// ``` 22 | /// #[derive(Debug, Clone, Copy, Stat)] 23 | /// #[stat(value = "StatIntPercentAdditive")] 24 | /// pub enum MyStat { 25 | /// #[default] A, 26 | /// B, 27 | /// C, 28 | /// } 29 | /// ``` 30 | /// 31 | /// * `#[default]` 32 | /// 33 | /// If specified, guarantees no panic even if a bad id 34 | /// is encountered, this likely will not happen in normal usage, 35 | /// as id is not used in serialization. 36 | #[proc_macro_error] 37 | #[proc_macro_derive(Stat, attributes(stat, default))] 38 | pub fn stat(tokens: TokenStream1) -> TokenStream1 { 39 | let input = parse_macro_input!(tokens as DeriveInput); 40 | let crate0 = quote! {::bevy_stat_query}; 41 | let name = input.ident; 42 | 43 | let mut value = None; 44 | 45 | for attr in input.attrs { 46 | if !attr.path().is_ident("stat") { 47 | continue; 48 | } 49 | let _ = attr.parse_nested_meta(|parse| { 50 | if !parse.path.is_ident("value") { 51 | return Ok(()); 52 | } 53 | let Ok(s) = parse.value()?.parse::() else { 54 | abort!(parse.path.span(), "Expected #[stat(value = \"StatValue\")]") 55 | }; 56 | value = match s.parse::() { 57 | Ok(v) => Some(v), 58 | Err(e) => abort!(s.span(), "{}", e), 59 | }; 60 | Ok(()) 61 | }); 62 | } 63 | 64 | let Some(value) = value else { 65 | abort!(Span::call_site(), "Expected #[stat(value = \"StatValue\")]") 66 | }; 67 | 68 | match input.data { 69 | syn::Data::Struct(s) => { 70 | let Fields::Unit = s.fields else { 71 | abort!(s.struct_token.span, "Only supports unit structs and enums."); 72 | }; 73 | quote! { 74 | impl #crate0::Stat for #name { 75 | type Value = #value; 76 | 77 | fn name(&self) -> &'static str { 78 | stringify!(#name) 79 | } 80 | 81 | fn vtable() -> &'static #crate0::StatVTable { 82 | #crate0::vtable!(#name) 83 | } 84 | 85 | fn as_index(&self) -> u64 { 86 | 0 87 | } 88 | 89 | fn from_index(_: u64) -> Self { 90 | #name 91 | } 92 | 93 | fn values() -> impl IntoIterator { 94 | [#name] 95 | } 96 | } 97 | } 98 | .into() 99 | } 100 | syn::Data::Enum(e) => { 101 | let mut default = quote! { 102 | panic!("Invalid value for {}: {}.", stringify!(#name), value) 103 | }; 104 | for v in &e.variants { 105 | let variant = &v.ident; 106 | if !matches!(v.fields, Fields::Unit) { 107 | abort!(v.span(), "Only fieldless enums are supported.") 108 | } 109 | for attr in &v.attrs { 110 | if attr.path().is_ident("default") { 111 | default = quote! {#name::#variant} 112 | } 113 | } 114 | } 115 | let names = e.variants.iter().map(|x| &x.ident); 116 | let names2 = e.variants.iter().map(|x| &x.ident); 117 | let names3 = e.variants.iter().map(|x| &x.ident); 118 | let names4 = e.variants.iter().map(|x| &x.ident); 119 | let mut last = 0u64; 120 | let indices: Vec<_> = e 121 | .variants 122 | .iter() 123 | .map(|x| match &x.discriminant { 124 | None => { 125 | last += 1; 126 | last 127 | } 128 | Some((_, expr)) => { 129 | let Ok(lit) = syn::parse2::(expr.into_token_stream()) else { 130 | abort!(expr.span(), "Expected a number"); 131 | }; 132 | let Ok(num) = lit.base10_parse::() else { 133 | abort!(expr.span(), "Expected unsigned number"); 134 | }; 135 | last = num; 136 | num 137 | } 138 | }) 139 | .collect(); 140 | 141 | quote! { 142 | impl #crate0::Stat for #name { 143 | type Value = #value; 144 | 145 | fn name(&self) -> &'static str { 146 | match self { 147 | #(#name::#names => stringify!(#names),)* 148 | } 149 | } 150 | 151 | fn vtable() -> &'static #crate0::StatVTable { 152 | #crate0::vtable!(#name) 153 | } 154 | 155 | fn as_index(&self) -> u64 { 156 | match self { 157 | #(#name::#names2 => #indices,)* 158 | } 159 | } 160 | 161 | fn from_index(value: u64) -> Self { 162 | match value { 163 | #(#indices => #name::#names3,)* 164 | _ => #default 165 | } 166 | } 167 | 168 | fn values() -> impl IntoIterator { 169 | [#(#name::#names4),*] 170 | } 171 | } 172 | } 173 | .into() 174 | } 175 | syn::Data::Union(u) => { 176 | abort!(u.union_token.span, "Only supports unit structs and enums."); 177 | } 178 | } 179 | } 180 | 181 | /// Allow the type to convert to `Attribute`. 182 | /// 183 | /// # Supported types 184 | /// * Unit struct 185 | /// * Fieldless enum with `#[repr(u64)]` 186 | /// * Newtype of u64 187 | /// 188 | /// This is usable with `bitflags!` in impl mode. 189 | #[proc_macro_error] 190 | #[proc_macro_derive(Attribute)] 191 | pub fn attribute(tokens: TokenStream1) -> TokenStream1 { 192 | let input = parse_macro_input!(tokens as DeriveInput); 193 | let crate0 = quote! {::bevy_stat_query}; 194 | let name = input.ident; 195 | let uniq = quote! { 196 | { 197 | #[used] 198 | static THING: ::std::sync::atomic::AtomicU8 = ::std::sync::atomic::AtomicU8::new(0); 199 | &THING as *const ::std::sync::atomic::AtomicU8 as usize 200 | } 201 | }; 202 | match input.data { 203 | syn::Data::Struct(s) => match s.fields { 204 | Fields::Named(_) => abort!( 205 | s.struct_token.span, 206 | "Only supports unit structs, bitflags and enums." 207 | ), 208 | Fields::Unnamed(fields) => { 209 | if fields.unnamed.len() != 1 { 210 | abort!( 211 | s.struct_token.span, 212 | "Only supports unit structs, bitflags and enums." 213 | ); 214 | } 215 | quote! { 216 | impl From<#name> for #crate0::Attribute<'static> { 217 | fn from(value: #name) -> #crate0::Attribute<'static> { 218 | #crate0::Attribute::Enum{ 219 | tag: #uniq, 220 | index: value.0 as u64, 221 | } 222 | } 223 | } 224 | 225 | impl From<&#name> for #crate0::Attribute<'static> { 226 | fn from(value: &#name) -> #crate0::Attribute<'static> { 227 | #crate0::Attribute::Enum{ 228 | tag: #uniq, 229 | index: value.0 as u64, 230 | } 231 | } 232 | } 233 | } 234 | .into() 235 | } 236 | Fields::Unit => quote! { 237 | impl From<#name> for #crate0::Attribute<'static> { 238 | fn from(_: #name) -> #crate0::Attribute<'static> { 239 | #crate0::Attribute::Enum{ 240 | tag: #uniq, 241 | index: 0, 242 | } 243 | } 244 | } 245 | 246 | impl From<&#name> for #crate0::Attribute<'static> { 247 | fn from(_: &#name) -> #crate0::Attribute<'static> { 248 | #crate0::Attribute::Enum{ 249 | tag: #uniq, 250 | index: 0, 251 | } 252 | } 253 | } 254 | } 255 | .into(), 256 | }, 257 | syn::Data::Enum(fields) => { 258 | let f1 = fields.variants.iter().map(|x| &x.ident); 259 | let f2 = fields.variants.iter().map(|x| &x.ident); 260 | quote! { 261 | impl From<#name> for #crate0::Attribute<'static> { 262 | fn from(value: #name) -> #crate0::Attribute<'static> { 263 | #crate0::Attribute::Enum{ 264 | tag: #uniq, 265 | index: value as u64, 266 | } 267 | } 268 | } 269 | 270 | impl From<&#name> for #crate0::Attribute<'static> { 271 | fn from(value: &#name) -> #crate0::Attribute<'static> { 272 | let variant = match value { 273 | #(#name::#f1 => #name::#f2),* 274 | }; 275 | #crate0::Attribute::Enum{ 276 | tag: #uniq, 277 | index: variant as u64, 278 | } 279 | } 280 | } 281 | } 282 | } 283 | .into(), 284 | syn::Data::Union(u) => { 285 | abort!( 286 | u.union_token.span, 287 | "Only supports unit structs, bitflags and enums." 288 | ); 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/attribute.rs: -------------------------------------------------------------------------------- 1 | /// Represents either a string or a typed enum. 2 | #[repr(C)] 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 4 | pub enum Attribute<'t> { 5 | String(&'t str), 6 | Enum { tag: usize, index: u64 }, 7 | } 8 | 9 | impl Attribute<'_> { 10 | pub fn is<'t, T: ?Sized>(&self, item: &'t T) -> bool 11 | where 12 | &'t T: Into>, 13 | { 14 | *self == item.into() 15 | } 16 | } 17 | 18 | impl PartialEq for Attribute<'_> { 19 | fn eq(&self, other: &String) -> bool { 20 | self.is(other) 21 | } 22 | } 23 | 24 | impl PartialEq<&str> for Attribute<'_> { 25 | fn eq(&self, other: &&str) -> bool { 26 | self.is(other) 27 | } 28 | } 29 | 30 | impl PartialEq for Attribute<'_> 31 | where 32 | for<'t> &'t T: Into>, 33 | { 34 | fn eq(&self, other: &T) -> bool { 35 | self.is(other) 36 | } 37 | } 38 | 39 | impl<'t> From<&'t str> for Attribute<'t> { 40 | fn from(val: &'t str) -> Self { 41 | Attribute::String(val) 42 | } 43 | } 44 | 45 | #[cfg(test)] 46 | mod test { 47 | use crate::Attribute; 48 | 49 | #[test] 50 | fn test_attribute_eq() { 51 | assert!(Attribute::String("Hello").is("Hello")); 52 | assert_eq!(Attribute::String("Hello"), "Hello"); 53 | assert_eq!(Attribute::String("Hello"), "Hello".to_owned()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::type_name, 3 | cell::UnsafeCell, 4 | mem::{align_of, size_of, MaybeUninit}, 5 | }; 6 | 7 | #[inline(always)] 8 | pub(crate) fn validate() { 9 | if !matches!(align_of::(), 1 | 2 | 4 | 8) { 10 | panic!( 11 | "{} has alignment {}. Stat::Value can only be values with alignments 1, 2, 4 or 8.", 12 | type_name::(), 13 | align_of::() 14 | ) 15 | } 16 | if size_of::() > 24 { 17 | panic!( 18 | "{} has size {}. Stat::Value can only be values up to 24 bytes.", 19 | type_name::(), 20 | size_of::() 21 | ) 22 | } 23 | } 24 | 25 | /// A type that should be able to hold everything in rust within constraints. 26 | /// 27 | /// # Compatibility 28 | /// 29 | /// This version requires [`UnsafeCell`] for soundness, if `Freeze` is stabilized, 30 | /// we might drop [`UnsafeCell`] for performance, thus preventing internally mutable 31 | /// types like `Mutex` from being used as `StatValue`. 32 | #[repr(C, align(8))] 33 | pub struct Buffer(UnsafeCell<[MaybeUninit; 24]>); 34 | 35 | unsafe impl Send for Buffer {} 36 | unsafe impl Sync for Buffer {} 37 | 38 | impl Buffer { 39 | /// Convert to a concrete item. 40 | pub(crate) unsafe fn as_ref(&self) -> &T { 41 | validate::(); 42 | unsafe { (self.0.get() as *const T).as_ref() }.unwrap() 43 | } 44 | 45 | /// Convert to a concrete item. 46 | pub(crate) unsafe fn as_mut(&mut self) -> &mut T { 47 | validate::(); 48 | unsafe { (self.0.get_mut().as_ptr() as *mut T).as_mut() }.unwrap() 49 | } 50 | 51 | /// Convert to a concrete item. 52 | pub(crate) unsafe fn into(mut self) -> T { 53 | validate::(); 54 | unsafe { (self.0.get_mut().as_ptr() as *mut T).read() } 55 | } 56 | 57 | /// Convert from a concrete item. 58 | pub(crate) fn from(item: T) -> Self { 59 | validate::(); 60 | let mut buffer = [MaybeUninit::uninit(); 24]; 61 | unsafe { (buffer.as_mut_ptr() as *mut T).write(item) }; 62 | Buffer(UnsafeCell::new(buffer)) 63 | } 64 | 65 | /// Read from a mutable reference to buffer. 66 | /// 67 | /// # Safety 68 | /// 69 | /// Buffer must not be read after and should be dropped immediately. 70 | pub(crate) unsafe fn read_move(&mut self) -> T { 71 | validate::(); 72 | unsafe { (self.0.get_mut().as_ptr() as *mut T).read() } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/cowstr.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt}; 2 | 3 | use serde::{ 4 | de::{Error, Visitor}, 5 | Deserializer, 6 | }; 7 | 8 | struct CowStrVisitor; 9 | 10 | impl<'de> Visitor<'de> for CowStrVisitor { 11 | type Value = Cow<'de, str>; 12 | 13 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 14 | formatter.write_str("a string") 15 | } 16 | 17 | fn visit_borrowed_str(self, value: &'de str) -> Result { 18 | Ok(Cow::Borrowed(value)) 19 | } 20 | 21 | fn visit_str(self, value: &str) -> Result { 22 | Ok(Cow::Owned(value.to_owned())) 23 | } 24 | 25 | fn visit_string(self, value: String) -> Result { 26 | Ok(Cow::Owned(value)) 27 | } 28 | } 29 | 30 | pub fn deserialize_cow_str<'de, D: Deserializer<'de>>( 31 | deserializer: D, 32 | ) -> Result, D::Error> { 33 | deserializer.deserialize_str(CowStrVisitor) 34 | } 35 | -------------------------------------------------------------------------------- /src/fraction.rs: -------------------------------------------------------------------------------- 1 | use std::ops::*; 2 | 3 | use bevy_reflect::TypePath; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{num_traits::Number, Float, Int, NumCast}; 7 | 8 | // Copied from the `gcd` crate by frewsxcv, MIT/Apache-2.0 9 | macro_rules! gcd { 10 | ($x: expr, $y: expr) => { 11 | 'gcd: { 12 | let mut u = $x; 13 | let mut v = $y; 14 | if u == 0 { 15 | break 'gcd v; 16 | } 17 | if v == 0 { 18 | break 'gcd u; 19 | } 20 | 21 | // abs 22 | #[allow(unused_comparisons)] 23 | if u < 0 { 24 | u = 0 - u 25 | }; 26 | #[allow(unused_comparisons)] 27 | if v < 0 { 28 | v = 0 - v 29 | }; 30 | 31 | let shift = (u | v).trailing_zeros(); 32 | u >>= shift; 33 | v >>= shift; 34 | u >>= u.trailing_zeros(); 35 | 36 | loop { 37 | v >>= v.trailing_zeros(); 38 | #[allow(clippy::manual_swap)] 39 | if u > v { 40 | // mem::swap(&mut u, &mut v); 41 | let temp = u; 42 | u = v; 43 | v = temp; 44 | } 45 | v -= u; // here v >= u 46 | if v == 0 { 47 | break; 48 | } 49 | } 50 | u << shift 51 | } 52 | }; 53 | } 54 | 55 | pub(crate) use gcd; 56 | 57 | /// Represents a fractional number. 58 | /// 59 | /// # Type Contract 60 | /// 61 | /// All combinations of numbers and signs are allowed, as long as denominator is not 0. 62 | /// Some operations like `new` will perform reduction on the value while others won't for performance. 63 | /// 64 | /// # Reductions 65 | /// 66 | /// Only `new` does full reduction, operators only do partial reduction for powers of 2. 67 | /// In the context of `bevy_stat_query`, use simple numbers like `1/3` over complicated 68 | /// ones like `33/100` to avoid integer overflows. 69 | #[derive(Debug, Clone, Copy, TypePath, Serialize, Deserialize)] 70 | #[repr(C)] 71 | pub struct Fraction { 72 | numer: I, 73 | denom: I, 74 | } 75 | 76 | impl Default for Fraction { 77 | fn default() -> Self { 78 | Self { 79 | numer: I::ZERO, 80 | denom: I::ONE, 81 | } 82 | } 83 | } 84 | 85 | impl From for Fraction { 86 | fn from(value: T) -> Self { 87 | Self::from_int(value) 88 | } 89 | } 90 | 91 | impl PartialEq for Fraction { 92 | fn eq(&self, other: &Self) -> bool { 93 | self.numer * other.denom == self.denom * other.numer 94 | } 95 | } 96 | 97 | impl Eq for Fraction {} 98 | 99 | impl PartialOrd for Fraction { 100 | fn partial_cmp(&self, other: &Self) -> Option { 101 | Some(self.cmp(other)) 102 | } 103 | } 104 | 105 | impl Ord for Fraction { 106 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 107 | (self.numer * other.denom).cmp(&(self.denom * other.numer)) 108 | } 109 | } 110 | 111 | macro_rules! impl_const_v { 112 | ($($ty: ident),* $(,)*) => { 113 | $(impl Fraction<$ty> { 114 | pub const fn const_new(numer: $ty, denom: $ty) -> Self { 115 | let gcd = gcd!(numer, denom); 116 | Fraction { 117 | numer: numer / gcd, 118 | denom: denom / gcd, 119 | } 120 | } 121 | 122 | pub const fn const_pct(percent: $ty) -> Self { 123 | Self::const_new(percent, 100) 124 | } 125 | 126 | pub const fn const_reduce(self) -> Self { 127 | Self::const_new(self.denom, self.numer) 128 | } 129 | })* 130 | }; 131 | } 132 | 133 | impl_const_v!(u8, u16, u32, u64, u128, usize); 134 | impl_const_v!(i8, i16, i32, i64, i128, isize); 135 | 136 | impl Fraction { 137 | pub fn new(numer: I, denom: I) -> Self { 138 | let gcd = numer.gcd(denom); 139 | Fraction { 140 | numer: numer / gcd, 141 | denom: denom / gcd, 142 | } 143 | } 144 | 145 | pub fn numer(&self) -> I { 146 | self.numer 147 | } 148 | 149 | pub fn denom(&self) -> I { 150 | self.denom 151 | } 152 | 153 | pub fn pct(percent: I) -> Self { 154 | Self::new(percent, I::from_i64(100)) 155 | } 156 | 157 | pub fn reduced_pow2(mut self) -> Self { 158 | self.numer.fast_reduction(&mut self.denom); 159 | self 160 | } 161 | 162 | pub fn reduce(&mut self) { 163 | *self = Self::new(self.numer, self.denom) 164 | } 165 | 166 | pub fn reduced(self) -> Self { 167 | Self::new(self.numer, self.denom) 168 | } 169 | 170 | /// Create a unreduced fraction, does not breach the type contract as long as `denom` is not 0. 171 | pub const fn new_raw(numer: I, denom: I) -> Self { 172 | Self { numer, denom } 173 | } 174 | 175 | /// Create the fraction `value / 1`. 176 | pub const fn from_int(value: I) -> Self { 177 | Self::new_raw(value, I::ONE) 178 | } 179 | 180 | /// Returns true if number is less than zero. 181 | pub fn is_positive(&self) -> bool { 182 | !((self.numer < I::ZERO) ^ (self.denom < I::ZERO)) && self.numer != I::ZERO 183 | } 184 | 185 | pub fn abs(&self) -> Self { 186 | Fraction { 187 | numer: self.numer.abs(), 188 | denom: self.denom.abs(), 189 | } 190 | } 191 | 192 | /// Returns true if number is zero. 193 | pub fn is_zero(&self) -> bool { 194 | self.numer == I::ZERO 195 | } 196 | 197 | /// Returns true if number is less than zero. 198 | pub fn is_negative(&self) -> bool { 199 | (self.numer < I::ZERO) ^ (self.denom < I::ZERO) && self.numer != I::ZERO 200 | } 201 | 202 | pub fn floor(self) -> I { 203 | if self.is_negative() { 204 | (self.numer - self.denom + self.denom.signum()) / self.denom 205 | } else { 206 | self.numer / self.denom 207 | } 208 | } 209 | 210 | pub fn ceil(self) -> I { 211 | if self.is_negative() { 212 | self.numer / self.denom 213 | } else { 214 | (self.numer + self.denom - self.denom.signum()) / self.denom 215 | } 216 | } 217 | 218 | pub fn trunc(self) -> I { 219 | self.numer / self.denom 220 | } 221 | 222 | pub fn round(self) -> I { 223 | if self.is_negative() { 224 | (self.numer - self.denom / (I::ONE + I::ONE)) / self.denom 225 | } else { 226 | (self.numer + self.denom / (I::ONE + I::ONE)) / self.denom 227 | } 228 | } 229 | 230 | /// Returns a truncated fraction part of this value. 231 | pub fn fract(self) -> Self { 232 | self - self.trunc() 233 | } 234 | 235 | /// Returns an integer and fraction part of this value. 236 | pub fn into_mixed_number(self) -> (I, Self) { 237 | let result = self.trunc(); 238 | (result, self - result) 239 | } 240 | 241 | /// Truncate the integer part and return it. 242 | pub fn to_mixed_number(&mut self) -> I { 243 | let result = self.trunc(); 244 | *self -= result; 245 | result 246 | } 247 | } 248 | 249 | impl NumCast for Fraction { 250 | fn cast(self) -> I { 251 | self.trunc() 252 | } 253 | } 254 | 255 | impl NumCast> for I { 256 | fn cast(self) -> Fraction { 257 | Fraction::from_int(self) 258 | } 259 | } 260 | 261 | macro_rules! impl_ops { 262 | ($t1: ident, $f1: ident, $t2: ident, $f2: ident, $a: ident, $b: ident, $e1: expr, $e2: expr) => { 263 | impl $t1 for Fraction { 264 | type Output = Self; 265 | 266 | fn $f1(self, rhs: Self) -> Self::Output { 267 | let $a = self; 268 | let $b = rhs; 269 | Fraction { 270 | numer: $e1, 271 | denom: $e2, 272 | } 273 | .reduced_pow2() 274 | } 275 | } 276 | 277 | impl $t1 for Fraction { 278 | type Output = Self; 279 | 280 | fn $f1(self, rhs: T) -> Self::Output { 281 | let $a = self; 282 | let $b = Fraction::from_int(rhs); 283 | Fraction { 284 | numer: $e1, 285 | denom: $e2, 286 | } 287 | .reduced_pow2() 288 | } 289 | } 290 | 291 | impl $t2 for Fraction { 292 | fn $f2(&mut self, rhs: Self) { 293 | let $a = *self; 294 | let $b = rhs; 295 | *self = Fraction { 296 | numer: $e1, 297 | denom: $e2, 298 | } 299 | .reduced_pow2() 300 | } 301 | } 302 | 303 | impl $t2 for Fraction { 304 | fn $f2(&mut self, rhs: T) { 305 | let $a = *self; 306 | let $b = Fraction::from_int(rhs); 307 | *self = Fraction { 308 | numer: $e1, 309 | denom: $e2, 310 | } 311 | .reduced_pow2() 312 | } 313 | } 314 | }; 315 | } 316 | 317 | impl_ops!( 318 | Add, 319 | add, 320 | AddAssign, 321 | add_assign, 322 | a, 323 | b, 324 | a.numer * b.denom + a.denom * b.numer, 325 | a.denom * b.denom 326 | ); 327 | impl_ops!( 328 | Sub, 329 | sub, 330 | SubAssign, 331 | sub_assign, 332 | a, 333 | b, 334 | a.numer * b.denom - a.denom * b.numer, 335 | a.denom * b.denom 336 | ); 337 | impl_ops!( 338 | Mul, 339 | mul, 340 | MulAssign, 341 | mul_assign, 342 | a, 343 | b, 344 | a.numer * b.numer, 345 | a.denom * b.denom 346 | ); 347 | impl_ops!( 348 | Div, 349 | div, 350 | DivAssign, 351 | div_assign, 352 | a, 353 | b, 354 | a.numer * b.denom, 355 | a.denom * b.numer 356 | ); 357 | 358 | impl Number for Fraction { 359 | const ZERO: Self = Fraction::new_raw(I::ZERO, I::ONE); 360 | const ONE: Self = Fraction::new_raw(I::ONE, I::ONE); 361 | const MIN_VALUE: Self = Fraction::new_raw(I::MIN_VALUE, I::ONE); 362 | const MAX_VALUE: Self = Fraction::new_raw(I::MAX_VALUE, I::ONE); 363 | 364 | fn _min(self, other: Self) -> Self { 365 | Ord::min(self, other) 366 | } 367 | 368 | fn _max(self, other: Self) -> Self { 369 | Ord::max(self, other) 370 | } 371 | } 372 | 373 | impl Float for Fraction { 374 | fn floor(self) -> Self { 375 | Fraction::from_int(Fraction::floor(self)) 376 | } 377 | 378 | fn ceil(self) -> Self { 379 | Fraction::from_int(Fraction::ceil(self)) 380 | } 381 | 382 | fn trunc(self) -> Self { 383 | Fraction::from_int(Fraction::trunc(self)) 384 | } 385 | 386 | fn round(self) -> Self { 387 | Fraction::from_int(Fraction::round(self)) 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | #![allow(clippy::too_many_arguments)] 3 | #![doc = include_str!("../README.md")] 4 | #[allow(unused)] 5 | use bevy_ecs::{component::Component, query::QueryData, system::SystemParam}; 6 | 7 | pub(crate) static TYPE_ERROR: &str = "Error: a stat does not have the appropriate type. \ 8 | This is almost certainly a bug since we do not provide a type erased api."; 9 | 10 | #[doc(hidden)] 11 | pub use bevy_app::{App, Plugin}; 12 | 13 | mod fraction; 14 | mod num_traits; 15 | pub use fraction::Fraction; 16 | pub use num_traits::{Flags, Float, Int, NumCast, Number}; 17 | mod stream; 18 | pub use stream::*; 19 | mod querier; 20 | pub use querier::*; 21 | mod qualifier; 22 | pub mod types; 23 | pub use qualifier::{Qualifier, QualifierFlag, QualifierQuery}; 24 | mod stat; 25 | #[cfg(feature = "derive")] 26 | pub use bevy_stat_query_derive::{Attribute, Stat}; 27 | pub(crate) use stat::StatExt; 28 | pub(crate) use stat::StatInst; 29 | pub use stat::{Stat, StatVTable, StatValuePair}; 30 | pub mod operations; 31 | pub use operations::StatValue; 32 | mod plugin; 33 | pub use plugin::{ 34 | GlobalStatDefaults, GlobalStatRelations, StatDeserializers, StatExtension, STAT_DESERIALIZERS, 35 | }; 36 | mod stat_map; 37 | pub use stat_map::StatMap; 38 | mod buffer; 39 | pub mod rounding; 40 | use std::fmt::Debug; 41 | mod attribute; 42 | pub use attribute::Attribute; 43 | mod cowstr; 44 | 45 | mod sealed { 46 | pub trait Sealed {} 47 | 48 | impl Sealed for T {} 49 | } 50 | 51 | /// Alias for `Clone + Debug + Send + Sync + 'static`. 52 | pub trait Shareable: Clone + Debug + Send + Sync + 'static {} 53 | impl Shareable for T where T: Clone + Debug + Send + Sync + 'static {} 54 | 55 | /// Construct a reference to a static [`StatVTable`] with serialization support. 56 | /// ``` 57 | /// vtable!(Type); 58 | /// ``` 59 | /// Equivalent to 60 | /// ``` 61 | /// { 62 | /// static VTABLE: StatVTable = StatVTable::of::(); 63 | /// &VTABLE 64 | /// } 65 | /// ``` 66 | #[macro_export] 67 | macro_rules! vtable { 68 | ($ty: ty) => {{ 69 | #[used] 70 | static _VTABLE: $crate::StatVTable<$ty> = $crate::StatVTable::of::<$ty>(); 71 | &_VTABLE 72 | }}; 73 | } 74 | 75 | /// Downcast [`StatValuePair`] to a concrete pair of stat and value. 76 | /// 77 | /// # Syntax 78 | /// 79 | /// ``` 80 | /// # /* 81 | /// match_stat!(stat_value_pair => { 82 | /// // if stat is `MyStat::A`, downcast the value to `MyStat::Value` as `value`. 83 | /// (MyStat::A, value) => { 84 | /// value.add(1); 85 | /// }, 86 | /// // if stat is `MyStat`, downcast the stat as `stat` and the value as `value`. 87 | /// (stat @ MyStat, value) => { 88 | /// value.add(1); 89 | /// }, 90 | /// } 91 | /// # */ 92 | /// ``` 93 | #[macro_export] 94 | macro_rules! match_stat { 95 | ($stat_value: expr => {($ident: ident @ $ty: ty, $value: pat) => $expr: expr $(, $($tt: tt)*)?}) => { 96 | if let Some(($ident, $value)) = $stat_value.cast::<$ty>() { 97 | $expr 98 | } $( 99 | else { 100 | $crate::match_stat!($stat_value => {$($tt)*}) 101 | } 102 | )? 103 | }; 104 | ($stat_value: expr => {($is: expr, $value: pat) => $expr: expr $(, $($tt: tt)*)?}) => { 105 | if let Some($value) = $stat_value.is_then_cast(&$is) { 106 | $expr 107 | } $( 108 | else { 109 | $crate::match_stat!($stat_value => {$($tt)*}) 110 | } 111 | )? 112 | }; 113 | ($stat_value: expr => {_ => $expr: expr $(,)?}) => { 114 | $expr 115 | }; 116 | // Matches the last comma case. 117 | ($stat_value: expr => {}) => {()}; 118 | } 119 | 120 | use buffer::{validate, Buffer}; 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use bevy_ecs::component::Component; 125 | use num_enum::{FromPrimitive, IntoPrimitive}; 126 | use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; 127 | 128 | use crate::{ 129 | stat::StatValuePair, 130 | types::{StatFlags, StatIntPercentAdditive}, 131 | Querier, Stat, StatStream, StatValue, 132 | }; 133 | 134 | #[derive(Component)] 135 | pub struct X; 136 | 137 | #[derive(Debug, Clone, Copy, IntoStaticStr, EnumIter, FromPrimitive, IntoPrimitive)] 138 | #[repr(u64)] 139 | pub enum IntStat { 140 | #[default] 141 | A, 142 | B, 143 | C, 144 | D, 145 | } 146 | 147 | impl Stat for IntStat { 148 | type Value = StatIntPercentAdditive; 149 | 150 | fn name(&self) -> &'static str { 151 | self.into() 152 | } 153 | 154 | fn vtable() -> &'static crate::StatVTable { 155 | vtable!(IntStat) 156 | } 157 | 158 | fn as_index(&self) -> u64 { 159 | (*self).into() 160 | } 161 | 162 | fn from_index(index: u64) -> Self { 163 | index.into() 164 | } 165 | 166 | fn values() -> impl IntoIterator { 167 | IntStat::iter() 168 | } 169 | } 170 | 171 | #[derive(Debug, Clone, Copy, IntoStaticStr, EnumIter, FromPrimitive, IntoPrimitive)] 172 | #[repr(u64)] 173 | pub enum FlagsStat { 174 | #[default] 175 | E, 176 | F, 177 | G, 178 | H, 179 | } 180 | 181 | impl Stat for FlagsStat { 182 | type Value = StatFlags; 183 | 184 | fn name(&self) -> &'static str { 185 | self.into() 186 | } 187 | 188 | fn vtable() -> &'static crate::StatVTable { 189 | vtable!(FlagsStat) 190 | } 191 | 192 | fn as_index(&self) -> u64 { 193 | (*self).into() 194 | } 195 | 196 | fn from_index(index: u64) -> Self { 197 | index.into() 198 | } 199 | 200 | fn values() -> impl IntoIterator { 201 | FlagsStat::iter() 202 | } 203 | } 204 | 205 | impl StatStream for X { 206 | type Qualifier = u32; 207 | 208 | fn stream_stat( 209 | &self, 210 | _: bevy::prelude::Entity, 211 | _: &crate::QualifierQuery, 212 | stat_value: &mut StatValuePair, 213 | _: Querier, 214 | ) { 215 | match_stat! { 216 | stat_value => { 217 | (IntStat::A, value) => { 218 | value.add(1); 219 | }, 220 | (IntStat::B, value) => { 221 | value.add(2); 222 | }, 223 | (v @ IntStat, value) => { 224 | value.add(v as i32); 225 | }, 226 | (FlagsStat::E, value) => { 227 | value.or(1); 228 | }, 229 | (v @ FlagsStat, value) => { 230 | value.or(v as i32); 231 | }, 232 | } 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/num_traits.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | fraction::{gcd, Fraction}, 3 | Shareable, 4 | }; 5 | use std::{ 6 | fmt::Debug, 7 | num::{Saturating, Wrapping}, 8 | ops::*, 9 | }; 10 | 11 | pub trait NumOps: 12 | Sized 13 | + Add 14 | + Sub 15 | + Mul 16 | + AddAssign 17 | + MulAssign 18 | { 19 | } 20 | 21 | impl NumOps for T where 22 | T: Sized 23 | + Add 24 | + Sub 25 | + Mul 26 | + AddAssign 27 | + MulAssign 28 | { 29 | } 30 | 31 | pub trait BitOps: 32 | Sized 33 | + BitAnd 34 | + BitOr 35 | + BitXor 36 | + BitAndAssign 37 | + BitOrAssign 38 | + BitXorAssign 39 | { 40 | } 41 | 42 | impl BitOps for T where 43 | T: Sized 44 | + BitAnd 45 | + BitOr 46 | + BitXor 47 | + BitAndAssign 48 | + BitOrAssign 49 | + BitXorAssign 50 | { 51 | } 52 | 53 | /// A type that can be treated as flags. 54 | /// 55 | /// Automatically implemented on types implementing all three bitwise operations `&|^`. 56 | pub trait Flags: 57 | BitOr + BitOrAssign + Debug + Default + Shareable 58 | { 59 | /// Exclude a portion of the flags. 60 | fn exclude(self, other: Self) -> Self; 61 | } 62 | 63 | impl Flags for T 64 | where 65 | T: BitOps + Debug + Default + Shareable, 66 | { 67 | fn exclude(self, other: Self) -> Self { 68 | self.clone() ^ (self & other) 69 | } 70 | } 71 | 72 | pub trait Number: NumOps + PartialOrd + Default + Copy + Shareable { 73 | const ZERO: Self; 74 | const ONE: Self; 75 | 76 | const MIN_VALUE: Self; 77 | const MAX_VALUE: Self; 78 | 79 | fn _min(self, other: Self) -> Self; 80 | fn _max(self, other: Self) -> Self; 81 | } 82 | 83 | /// Trait for an integer. 84 | pub trait Int: Number + Div + BitOps + Ord { 85 | fn from_i64(value: i64) -> Self; 86 | 87 | fn as_f32(self) -> f32; 88 | fn as_f64(self) -> f64; 89 | 90 | fn from_f32(value: f32) -> Self; 91 | fn from_f64(value: f64) -> Self; 92 | 93 | fn abs(self) -> Self; 94 | fn signum(self) -> Self; 95 | 96 | fn gcd(self, other: Self) -> Self; 97 | #[doc(hidden)] 98 | fn fast_reduction(&mut self, other: &mut Self); 99 | 100 | type PrimInt: Int + Clone + Shareable; 101 | 102 | fn into_fraction(self) -> Fraction; 103 | fn build_fraction(self, denom: Self) -> Fraction; 104 | fn from_fraction(frac: Fraction) -> Self; 105 | } 106 | 107 | macro_rules! impl_int { 108 | ($($ty: ty),* $(,)?) => { 109 | $( 110 | impl Number for $ty { 111 | const ZERO: Self = 0; 112 | const ONE: Self = 1; 113 | 114 | const MIN_VALUE: Self = <$ty>::MIN; 115 | const MAX_VALUE: Self = <$ty>::MAX; 116 | 117 | fn _min(self, other: Self) -> Self { 118 | Ord::min(self, other) 119 | } 120 | 121 | fn _max(self, other: Self) -> Self { 122 | Ord::max(self, other) 123 | } 124 | } 125 | 126 | impl Int for $ty { 127 | fn from_i64(value: i64) -> Self{ 128 | value.clamp(Self::MIN as i64, Self::MAX as i64) as Self 129 | } 130 | 131 | fn as_f32(self) -> f32 { 132 | self as f32 133 | } 134 | 135 | fn as_f64(self) -> f64 { 136 | self as f64 137 | } 138 | 139 | fn from_f32(value: f32) -> Self{ 140 | value as Self 141 | } 142 | 143 | fn from_f64(value: f64) -> Self{ 144 | value as Self 145 | } 146 | 147 | fn abs(self) -> Self { 148 | #[allow(unused_comparisons)] 149 | if self < 0 { 150 | 0 - self 151 | } else { 152 | self 153 | } 154 | } 155 | 156 | fn signum(self) -> Self { 157 | #[allow(unused_comparisons, arithmetic_overflow)] 158 | if self < 0 { 159 | 0 - 1 160 | } else if self == 0 { 161 | 0 162 | } else { 163 | 1 164 | } 165 | } 166 | 167 | fn fast_reduction(&mut self, other: &mut Self) { 168 | let u = self.abs(); 169 | let v = other.abs(); 170 | let shift = (u | v).trailing_zeros(); 171 | *self >>= shift; 172 | *other >>= shift; 173 | } 174 | 175 | fn gcd(self, other: Self) -> Self { 176 | gcd!(self, other) 177 | } 178 | 179 | type PrimInt = $ty; 180 | 181 | fn into_fraction(self) -> Fraction { 182 | Fraction::new_raw(self, 1) 183 | } 184 | 185 | fn build_fraction(self, denom: Self) -> Fraction { 186 | Fraction::new(self, denom) 187 | } 188 | 189 | fn from_fraction(frac: Fraction) -> Self{ 190 | frac.trunc() 191 | } 192 | })* 193 | }; 194 | } 195 | 196 | impl_int!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize,); 197 | 198 | macro_rules! impl_int_newtype { 199 | ($($base: ident {$($ty: ty),* $(,)?}),* $(,)?) => { 200 | $($( 201 | impl Number for $base<$ty> { 202 | const ZERO: Self = Self(0); 203 | const ONE: Self = Self(1); 204 | 205 | const MIN_VALUE: Self = Self(<$ty>::MIN); 206 | const MAX_VALUE: Self = Self(<$ty>::MAX); 207 | 208 | fn _min(self, other: Self) -> Self { 209 | Ord::min(self, other) 210 | } 211 | 212 | fn _max(self, other: Self) -> Self { 213 | Ord::max(self, other) 214 | } 215 | } 216 | 217 | impl Int for $base<$ty> { 218 | fn from_i64(value: i64) -> Self{ 219 | Self(value.clamp(<$ty>::MIN as i64, <$ty>::MAX as i64) as $ty) 220 | } 221 | 222 | fn as_f32(self) -> f32 { 223 | self.0 as f32 224 | } 225 | 226 | fn as_f64(self) -> f64 { 227 | self.0 as f64 228 | } 229 | 230 | fn from_f32(value: f32) -> Self{ 231 | Self(value as $ty) 232 | } 233 | 234 | fn from_f64(value: f64) -> Self{ 235 | Self(value as $ty) 236 | } 237 | 238 | fn abs(self) -> Self { 239 | #[allow(unused_comparisons)] 240 | if self < Self(0) { 241 | Self(0 - self.0) 242 | } else { 243 | self 244 | } 245 | } 246 | 247 | fn signum(self) -> Self { 248 | #[allow(unused_comparisons, arithmetic_overflow)] 249 | Self(if self.0 < 0 { 250 | 0 - 1 251 | } else if self.0 == 0 { 252 | 0 253 | } else { 254 | 1 255 | }) 256 | } 257 | 258 | 259 | fn fast_reduction(&mut self, other: &mut Self) { 260 | let u = self.0.abs(); 261 | let v = other.0.abs(); 262 | let shift = (u | v).trailing_zeros(); 263 | self.0 >>= shift; 264 | other.0 >>= shift; 265 | } 266 | 267 | fn gcd(self, other: Self) -> Self { 268 | Self(gcd!(self.0, other.0)) 269 | } 270 | 271 | type PrimInt = $ty; 272 | 273 | fn into_fraction(self) -> Fraction { 274 | Fraction::new_raw(self.0, 1) 275 | } 276 | 277 | fn build_fraction(self, denom: Self) -> Fraction { 278 | Fraction::new(self.0, denom.0) 279 | } 280 | 281 | fn from_fraction(frac: Fraction) -> Self{ 282 | Self(frac.trunc()) 283 | } 284 | })*)* 285 | }; 286 | } 287 | 288 | impl_int_newtype!( 289 | Wrapping { 290 | u8, 291 | u16, 292 | u32, 293 | u64, 294 | u128, 295 | usize, 296 | i8, 297 | i16, 298 | i32, 299 | i64, 300 | i128, 301 | isize, 302 | }, 303 | Saturating { 304 | u8, 305 | u16, 306 | u32, 307 | u64, 308 | u128, 309 | usize, 310 | i8, 311 | i16, 312 | i32, 313 | i64, 314 | i128, 315 | isize, 316 | }, 317 | ); 318 | 319 | /// Trait for a floating point number or a [`Fraction`]. 320 | pub trait Float: Number { 321 | fn floor(self) -> Self; 322 | fn ceil(self) -> Self; 323 | fn trunc(self) -> Self; 324 | fn round(self) -> Self; 325 | } 326 | 327 | impl Number for f32 { 328 | const ZERO: Self = 0.0; 329 | const ONE: Self = 1.0; 330 | const MIN_VALUE: Self = f32::MIN; 331 | const MAX_VALUE: Self = f32::MAX; 332 | 333 | fn _min(self, other: Self) -> Self { 334 | self.min(other) 335 | } 336 | 337 | fn _max(self, other: Self) -> Self { 338 | self.max(other) 339 | } 340 | } 341 | 342 | impl Float for f32 { 343 | fn floor(self) -> Self { 344 | self.floor() 345 | } 346 | 347 | fn ceil(self) -> Self { 348 | self.ceil() 349 | } 350 | 351 | fn trunc(self) -> Self { 352 | self.trunc() 353 | } 354 | 355 | fn round(self) -> Self { 356 | self.round() 357 | } 358 | } 359 | 360 | impl Number for f64 { 361 | const ZERO: Self = 0.0; 362 | const ONE: Self = 1.0; 363 | const MIN_VALUE: Self = f64::MIN; 364 | const MAX_VALUE: Self = f64::MAX; 365 | 366 | fn _min(self, other: Self) -> Self { 367 | self.min(other) 368 | } 369 | 370 | fn _max(self, other: Self) -> Self { 371 | self.max(other) 372 | } 373 | } 374 | 375 | impl Float for f64 { 376 | fn floor(self) -> Self { 377 | self.floor() 378 | } 379 | 380 | fn ceil(self) -> Self { 381 | self.ceil() 382 | } 383 | 384 | fn trunc(self) -> Self { 385 | self.trunc() 386 | } 387 | 388 | fn round(self) -> Self { 389 | self.round() 390 | } 391 | } 392 | 393 | pub trait NumCast { 394 | fn cast(self) -> T; 395 | } 396 | 397 | impl NumCast for I { 398 | fn cast(self) -> f32 { 399 | self.as_f32() 400 | } 401 | } 402 | 403 | impl NumCast for I { 404 | fn cast(self) -> f64 { 405 | self.as_f64() 406 | } 407 | } 408 | 409 | impl NumCast for f32 { 410 | fn cast(self) -> I { 411 | I::from_f32(self) 412 | } 413 | } 414 | 415 | impl NumCast for f64 { 416 | fn cast(self) -> I { 417 | I::from_f64(self) 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/operations.rs: -------------------------------------------------------------------------------- 1 | use bevy_reflect::TypePath; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// An single step unordered operation on a [`StatValue`]. 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] 6 | pub enum StatOperation { 7 | Add(S::Add), 8 | Mul(S::Mul), 9 | Or(S::Bit), 10 | Min(S::Bounds), 11 | Max(S::Bounds), 12 | Base(S::Base), 13 | } 14 | 15 | pub use StatOperation::*; 16 | 17 | use crate::Shareable; 18 | 19 | impl StatOperation { 20 | pub fn write_to(&self, to: &mut S) { 21 | match self.clone() { 22 | StatOperation::Add(item) => to.add(item), 23 | StatOperation::Mul(item) => to.mul(item), 24 | StatOperation::Or(item) => to.or(item), 25 | StatOperation::Min(item) => to.min(item), 26 | StatOperation::Max(item) => to.max(item), 27 | StatOperation::Base(item) => *to = S::from_base(item), 28 | } 29 | } 30 | 31 | pub fn into_stat(self) -> S { 32 | let mut v = S::default(); 33 | self.write_to(&mut v); 34 | v 35 | } 36 | } 37 | 38 | /// A never type indicating an operation is not supported. 39 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, TypePath, Serialize, Deserialize)] 40 | pub enum Unsupported {} 41 | 42 | /// Defines unordered operations on a stat's value. 43 | #[allow(unused_variables)] 44 | pub trait StatValue: Shareable + Default { 45 | type Out: Shareable + Default; 46 | 47 | fn join(&mut self, other: Self); 48 | 49 | fn join_by_ref(&mut self, other: &Self) { 50 | self.join(other.clone()) 51 | } 52 | 53 | fn eval(&self) -> Self::Out; 54 | 55 | type Add: Shareable; 56 | type Mul: Shareable; 57 | type Bit: Shareable; 58 | type Bounds: Shareable; 59 | type Base: Shareable; 60 | 61 | fn add(&mut self, other: Self::Add) {} 62 | fn mul(&mut self, other: Self::Mul) {} 63 | fn or(&mut self, other: Self::Bit) {} 64 | 65 | fn min(&mut self, other: Self::Bounds) {} 66 | fn max(&mut self, other: Self::Bounds) {} 67 | 68 | fn with_add(mut self, other: Self::Add) -> Self { 69 | self.add(other); 70 | self 71 | } 72 | 73 | fn with_mul(mut self, other: Self::Mul) -> Self { 74 | self.mul(other); 75 | self 76 | } 77 | 78 | fn with_min(mut self, other: Self::Bounds) -> Self { 79 | self.min(other); 80 | self 81 | } 82 | 83 | fn with_max(mut self, other: Self::Bounds) -> Self { 84 | self.max(other); 85 | self 86 | } 87 | 88 | fn with_or(mut self, other: Self::Bit) -> Self { 89 | self.or(other); 90 | self 91 | } 92 | 93 | fn with_join(mut self, other: Self) -> Self { 94 | self.join(other); 95 | self 96 | } 97 | 98 | fn with_join_ref(mut self, other: &Self) -> Self { 99 | self.join_by_ref(other); 100 | self 101 | } 102 | 103 | fn from_base(base: Self::Base) -> Self; 104 | } 105 | 106 | impl StatValue for bool { 107 | type Out = bool; 108 | 109 | fn join(&mut self, other: Self) { 110 | *self |= other 111 | } 112 | 113 | fn eval(&self) -> Self::Out { 114 | *self 115 | } 116 | 117 | type Add = Unsupported; 118 | 119 | type Mul = Unsupported; 120 | 121 | type Bit = Self; 122 | 123 | type Bounds = Unsupported; 124 | 125 | type Base = Self; 126 | 127 | fn from_base(base: Self::Base) -> Self { 128 | base 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::str::FromStr; 3 | 4 | use crate::operations::StatOperation; 5 | use crate::StatInst; 6 | use crate::{ 7 | Buffer, QualifierFlag, QualifierQuery, Querier, Stat, StatExt, StatStream, StatValue, 8 | StatValuePair, 9 | }; 10 | use bevy_app::App; 11 | use bevy_ecs::entity::Entity; 12 | use bevy_ecs::resource::Resource; 13 | use bevy_ecs::world::World; 14 | use bevy_reflect::TypePath; 15 | use rustc_hash::FxHashMap; 16 | 17 | type Bounds = <::Value as StatValue>::Bounds; 18 | 19 | /// Extension on [`World`] and [`App`] 20 | pub trait StatExtension { 21 | /// Register associated serialization routine for a stat. 22 | /// 23 | /// # Panics 24 | /// 25 | /// If trying to replace a previous stat entry with a different value. 26 | fn register_stat(&mut self) -> &mut Self; 27 | 28 | /// Register associated serialization routine for a stat by parsing a string. 29 | fn register_stat_parser( 30 | &mut self, 31 | f: impl FnMut(&str) -> Option + Send + Sync + 'static, 32 | ) -> &mut Self; 33 | 34 | /// Register associated serialization routine for a stat using its [`FromStr`] implementation. 35 | fn register_stat_from_str(&mut self) -> &mut Self; 36 | 37 | /// Register a default stat value. 38 | /// 39 | /// This is the standard way 40 | /// to add default bounds to a stat, e.g, in `1..=15`. 41 | fn register_stat_default(&mut self, stat: S, value: S::Value) -> &mut Self; 42 | 43 | /// Register the minimum value of a stat. 44 | fn register_stat_min(&mut self, stat: &S, value: Bounds) -> &mut Self; 45 | 46 | /// Register the maximum value of a stat. 47 | fn register_stat_max(&mut self, stat: &S, value: Bounds) -> &mut Self; 48 | 49 | /// Register a global stat relation 50 | /// that will be run on every stat query. 51 | fn register_stat_relation( 52 | &mut self, 53 | relation: impl Fn(Entity, &QualifierQuery, &mut StatValuePair, Querier) 54 | + Send 55 | + Sync 56 | + 'static, 57 | ) -> &mut Self; 58 | } 59 | 60 | impl StatExtension for World { 61 | fn register_stat(&mut self) -> &mut Self { 62 | self.get_resource_or_insert_with::(Default::default) 63 | .register::(); 64 | self 65 | } 66 | 67 | fn register_stat_from_str(&mut self) -> &mut Self { 68 | self.get_resource_or_insert_with::(Default::default) 69 | .register_parser_ok(T::from_str); 70 | self 71 | } 72 | 73 | fn register_stat_parser( 74 | &mut self, 75 | f: impl FnMut(&str) -> Option + Send + Sync + 'static, 76 | ) -> &mut Self { 77 | self.get_resource_or_insert_with::(Default::default) 78 | .register_parser(f); 79 | self 80 | } 81 | 82 | fn register_stat_default(&mut self, stat: S, value: S::Value) -> &mut Self { 83 | self.get_resource_or_insert_with::(Default::default) 84 | .insert(stat, value); 85 | self 86 | } 87 | 88 | fn register_stat_min(&mut self, stat: &S, value: Bounds) -> &mut Self { 89 | self.get_resource_or_insert_with::(Default::default) 90 | .patch(stat, StatOperation::Min(value)); 91 | self 92 | } 93 | 94 | fn register_stat_max(&mut self, stat: &S, value: Bounds) -> &mut Self { 95 | self.get_resource_or_insert_with::(Default::default) 96 | .patch(stat, StatOperation::Max(value)); 97 | self 98 | } 99 | 100 | fn register_stat_relation( 101 | &mut self, 102 | relation: impl Fn(Entity, &QualifierQuery, &mut StatValuePair, Querier) 103 | + Send 104 | + Sync 105 | + 'static, 106 | ) -> &mut Self { 107 | self.get_resource_or_insert_with(GlobalStatRelations::::default) 108 | .push(relation); 109 | self 110 | } 111 | } 112 | 113 | impl StatExtension for App { 114 | fn register_stat(&mut self) -> &mut Self { 115 | self.world_mut().register_stat::(); 116 | self 117 | } 118 | 119 | fn register_stat_from_str(&mut self) -> &mut Self { 120 | self.world_mut().register_stat_from_str::(); 121 | self 122 | } 123 | 124 | fn register_stat_parser( 125 | &mut self, 126 | f: impl FnMut(&str) -> Option + Send + Sync + 'static, 127 | ) -> &mut Self { 128 | self.world_mut().register_stat_parser::(f); 129 | self 130 | } 131 | 132 | fn register_stat_default(&mut self, stat: S, value: S::Value) -> &mut Self { 133 | self.world_mut().register_stat_default::(stat, value); 134 | self 135 | } 136 | 137 | fn register_stat_min(&mut self, stat: &S, value: Bounds) -> &mut Self { 138 | self.world_mut().register_stat_min(stat, value); 139 | self 140 | } 141 | 142 | fn register_stat_max(&mut self, stat: &S, value: Bounds) -> &mut Self { 143 | self.world_mut().register_stat_max(stat, value); 144 | self 145 | } 146 | 147 | fn register_stat_relation( 148 | &mut self, 149 | relation: impl Fn(Entity, &QualifierQuery, &mut StatValuePair, Querier) 150 | + Send 151 | + Sync 152 | + 'static, 153 | ) -> &mut Self { 154 | self.world_mut().register_stat_relation(relation); 155 | self 156 | } 157 | } 158 | 159 | /// [`Resource`] that stores default [`StatValue`]s per [`Stat`]. 160 | /// 161 | /// Stats that are not registered are still returned with [`Default::default()`] instead. 162 | #[derive(Resource, Default, TypePath)] 163 | pub struct GlobalStatDefaults { 164 | stats: FxHashMap, 165 | } 166 | 167 | impl std::fmt::Debug for GlobalStatDefaults { 168 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 169 | #[derive(Debug)] 170 | struct Stat(&'static str); 171 | let mut map = f.debug_map(); 172 | for (s, b) in &self.stats { 173 | map.entry(&Stat(s.name()), unsafe { (s.vtable.as_debug)(b) }); 174 | } 175 | map.finish() 176 | } 177 | } 178 | 179 | impl GlobalStatDefaults { 180 | pub fn new() -> Self { 181 | Self { 182 | stats: FxHashMap::default(), 183 | } 184 | } 185 | 186 | /// Insert a [`Stat`] and its associated default value. 187 | pub fn insert(&mut self, stat: S, value: S::Value) { 188 | self.stats.insert(stat.as_entry(), Buffer::from(value)); 189 | } 190 | 191 | /// Modify a [`Stat`]'s default value. 192 | pub fn patch(&mut self, stat: &S, value: StatOperation) { 193 | let stat = stat.as_entry(); 194 | match self.stats.get_mut(&stat) { 195 | Some(v) => value.write_to(unsafe { v.as_mut() }), 196 | None => { 197 | self.stats.insert(stat, { 198 | let mut stat = S::Value::default(); 199 | value.write_to(&mut stat); 200 | Buffer::from(stat) 201 | }); 202 | } 203 | } 204 | } 205 | 206 | /// Obtain a [`Stat`]'s default value. 207 | pub fn get(&self, stat: &S) -> S::Value { 208 | self.stats 209 | .get(&stat.as_entry()) 210 | .map(|x| unsafe { x.as_ref() }) 211 | .cloned() 212 | .unwrap_or(Default::default()) 213 | } 214 | 215 | /// Obtain a [`Stat`]'s default value. 216 | pub(crate) fn get_dyn(&self, stat: StatInst) -> Buffer { 217 | self.stats 218 | .get(&stat) 219 | .map(|x| unsafe { stat.clone_buffer(x) }) 220 | .unwrap_or((stat.vtable.default)()) 221 | } 222 | } 223 | 224 | impl Drop for GlobalStatDefaults { 225 | fn drop(&mut self) { 226 | for (k, v) in self.stats.iter_mut() { 227 | unsafe { k.drop_buffer(v) }; 228 | } 229 | } 230 | } 231 | 232 | /// [`Resource`] that stores global [`StatStream`]s that runs on every query. 233 | #[derive(Resource, TypePath)] 234 | pub struct GlobalStatRelations { 235 | stats: 236 | Vec, &mut StatValuePair, Querier) + Send + Sync>>, 237 | } 238 | 239 | impl Debug for GlobalStatRelations { 240 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 241 | f.debug_struct("GlobalStatRelations") 242 | .finish_non_exhaustive() 243 | } 244 | } 245 | 246 | impl Default for GlobalStatRelations { 247 | fn default() -> Self { 248 | Self { stats: Vec::new() } 249 | } 250 | } 251 | 252 | impl GlobalStatRelations { 253 | pub fn push( 254 | &mut self, 255 | stream: impl Fn(Entity, &QualifierQuery, &mut StatValuePair, Querier) 256 | + Send 257 | + Sync 258 | + 'static, 259 | ) -> &mut Self { 260 | self.stats.push(Box::new(stream)); 261 | self 262 | } 263 | 264 | pub fn with( 265 | mut self, 266 | stream: impl Fn(Entity, &QualifierQuery, &mut StatValuePair, Querier) 267 | + Send 268 | + Sync 269 | + 'static, 270 | ) -> Self { 271 | self.stats.push(Box::new(stream)); 272 | self 273 | } 274 | } 275 | 276 | impl StatStream for GlobalStatRelations { 277 | type Qualifier = Q; 278 | 279 | fn stream_stat( 280 | &self, 281 | entity: Entity, 282 | qualifier: &crate::QualifierQuery, 283 | stat_value: &mut crate::StatValuePair, 284 | querier: crate::Querier, 285 | ) { 286 | for f in self.stats.iter() { 287 | f(entity, qualifier, stat_value, querier) 288 | } 289 | } 290 | } 291 | 292 | /// Resource containing a name to instance map of [`Stat`]s. 293 | #[derive(Resource, Default)] 294 | pub struct StatDeserializers { 295 | pub(crate) concrete: FxHashMap<&'static str, StatInst>, 296 | pub(crate) parse_fns: Vec Option + Send + Sync>>, 297 | } 298 | 299 | impl Debug for StatDeserializers { 300 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 301 | f.debug_struct("StatInstances") 302 | .field("concrete", &self.concrete) 303 | .finish() 304 | } 305 | } 306 | 307 | impl StatDeserializers { 308 | /// Register all members of a [`Stat`]. 309 | /// 310 | /// # Panics 311 | /// 312 | /// If a stat registered conflicts with a previous entry. 313 | pub fn register(&mut self) { 314 | T::values().into_iter().for_each(|x| { 315 | if let Some(prev) = self.concrete.get(x.name()) { 316 | assert_eq!(prev, &x.as_entry(), "duplicate key {}", x.name()) 317 | } else { 318 | self.concrete.insert(x.name(), x.as_entry()); 319 | } 320 | }) 321 | } 322 | 323 | /// Register all members of a [`Stat`]. 324 | /// 325 | /// Always replaces a registered [`Stat`] of the same key. 326 | pub fn register_replace(&mut self) { 327 | T::values().into_iter().for_each(|x| { 328 | self.concrete.insert(x.name(), x.as_entry()); 329 | }) 330 | } 331 | 332 | /// Register a parser to a stat, ones inserted first has priority. 333 | pub fn register_parser( 334 | &mut self, 335 | mut f: impl FnMut(&str) -> Option + Send + Sync + 'static, 336 | ) { 337 | self.parse_fns 338 | .push(Box::new(move |x| f(x).map(|x| x.as_entry()))); 339 | } 340 | 341 | /// Register a parser to a stat, ones inserted first has priority. 342 | pub fn register_parser_ok( 343 | &mut self, 344 | mut f: impl FnMut(&str) -> Result + Send + Sync + 'static, 345 | ) { 346 | self.parse_fns 347 | .push(Box::new(move |x| f(x).map(|x| x.as_entry()).ok())); 348 | } 349 | 350 | pub fn get(&mut self, name: &str) -> Option { 351 | if let Some(concrete) = self.concrete.get(name) { 352 | return Some(*concrete); 353 | } 354 | for parser in &mut self.parse_fns { 355 | if let Some(result) = parser(name) { 356 | return Some(result); 357 | } 358 | } 359 | None 360 | } 361 | } 362 | 363 | scoped_tls_hkt::scoped_thread_local!( 364 | pub static mut STAT_DESERIALIZERS: StatDeserializers 365 | ); 366 | -------------------------------------------------------------------------------- /src/qualifier.rs: -------------------------------------------------------------------------------- 1 | use bevy_reflect::Reflect; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ 4 | fmt::Debug, 5 | hash::Hash, 6 | ops::{BitAnd, BitOr}, 7 | }; 8 | 9 | #[allow(unused)] 10 | use crate::{Shareable, Stat}; 11 | 12 | /// A flags like [`Qualifier`] for stats, normally bitflags or a set. 13 | /// 14 | /// An application should ideally implement one [`QualifierFlag`] and multiple [`Stat`]s, 15 | /// since different types of stats can still interop if they use the same [`QualifierFlag`]. 16 | pub trait QualifierFlag: BitOr + Ord + Hash + Shareable { 17 | fn contains(&self, other: &Self) -> bool; 18 | fn intersects(&self, other: &Self) -> bool; 19 | fn is_none_or_intersects(&self, other: &Self) -> bool { 20 | self.is_none() || self.intersects(other) 21 | } 22 | fn set_equals(&self, other: &Self) -> bool; 23 | fn none() -> Self; 24 | fn is_none(&self) -> bool; 25 | } 26 | 27 | impl QualifierFlag for T 28 | where 29 | T: BitOr 30 | + Ord 31 | + Hash 32 | + BitAnd 33 | + Default 34 | + Shareable 35 | + Copy, 36 | { 37 | fn contains(&self, other: &Self) -> bool { 38 | (*self & *other) == *other 39 | } 40 | 41 | fn set_equals(&self, other: &Self) -> bool { 42 | self == other 43 | } 44 | 45 | fn intersects(&self, other: &Self) -> bool { 46 | !(*self & *other).is_none() 47 | } 48 | 49 | fn none() -> Self { 50 | Self::default() 51 | } 52 | 53 | fn is_none(&self) -> bool { 54 | self == &Self::default() 55 | } 56 | } 57 | 58 | /// Data side qualifier for a stat. 59 | /// 60 | /// # When stored 61 | /// 62 | /// * `any_of` requires one or more conditions present. 63 | /// * `all_of` requires all conditions present. 64 | /// 65 | /// # Example 66 | /// 67 | /// ``` 68 | /// // Requires 'fire' to receive buff from 'fire damage'. 69 | /// let fire = QualifierFlags::all_of(Fire); 70 | /// // Requires both 'ice' and 'piercing' to receive buff from 'ice piercing damage' 71 | /// let ice_piercing = QualifierFlags::all_of(Ice | Piercing); 72 | /// // Requires at least one of the elements to receive buff from 'elemental damage'. 73 | /// let elemental = QualifierFlags::any_of(Fire | Water | Earth | Air); 74 | /// // Requires one of the elements and 'piercing'. 75 | /// let elemental_piercing = QualifierFlags::any_of(Fire | Water | Earth | Air) 76 | /// .and_all_of(Piercing); 77 | /// ``` 78 | #[derive( 79 | Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect, Serialize, Deserialize, 80 | )] 81 | pub struct Qualifier { 82 | pub all_of: Q, 83 | pub any_of: Q, 84 | } 85 | 86 | impl Default for Qualifier { 87 | fn default() -> Self { 88 | Self { 89 | any_of: Q::none(), 90 | all_of: Q::none(), 91 | } 92 | } 93 | } 94 | 95 | impl From for Qualifier { 96 | fn from(value: Q) -> Self { 97 | Qualifier { 98 | all_of: value, 99 | any_of: Q::none(), 100 | } 101 | } 102 | } 103 | 104 | impl Qualifier { 105 | pub fn none() -> Self { 106 | Self { 107 | any_of: Q::none(), 108 | all_of: Q::none(), 109 | } 110 | } 111 | 112 | pub fn is_none(&self) -> bool { 113 | self.any_of.is_none() && self.all_of.is_none() 114 | } 115 | 116 | pub fn any_of(qualifier: Q) -> Self { 117 | Self { 118 | any_of: qualifier, 119 | all_of: Q::none(), 120 | } 121 | } 122 | 123 | pub fn all_of(qualifier: Q) -> Self { 124 | Self { 125 | any_of: Q::none(), 126 | all_of: qualifier, 127 | } 128 | } 129 | 130 | pub fn and_any_of(self, qualifier: Q) -> Self { 131 | Self { 132 | any_of: self.any_of | qualifier, 133 | all_of: self.all_of, 134 | } 135 | } 136 | 137 | pub fn and_all_of(self, qualifier: Q) -> Self { 138 | Self { 139 | any_of: self.any_of, 140 | all_of: self.all_of | qualifier, 141 | } 142 | } 143 | 144 | /// # Examples 145 | /// * `elemental_damage` qualifies as `fire_damage`. 146 | /// * `fire_sword_damage` does not qualify as `fire_damage`. 147 | /// * `fire_damage` does not qualify as `elemental_damage`. 148 | /// * `fire_water_earth_air_damage` does not qualify as `elemental_damage`, 149 | pub fn qualifies_as(&self, queried: &QualifierQuery) -> bool { 150 | match queried { 151 | QualifierQuery::Aggregate(some_of) => { 152 | some_of.contains(&self.all_of) && self.any_of.is_none_or_intersects(some_of) 153 | } 154 | QualifierQuery::Exact { any_of, all_of } => { 155 | self.any_of.contains(any_of) && &self.all_of == all_of 156 | } 157 | } 158 | } 159 | } 160 | 161 | /// Query version of [`Qualifier`]. 162 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)] 163 | pub enum QualifierQuery { 164 | /// Look for qualifier that qualifies as this. 165 | /// 166 | /// Queried `any_of` intersects this (or is none) and this contains Queried `all_of`. 167 | Aggregate(Q), 168 | /// Look for qualifiers that are this and deny more generalized qualifiers. 169 | Exact { 170 | /// Queried `any_of` contains this. 171 | any_of: Q, 172 | /// Queried `all_of` equals this. 173 | all_of: Q, 174 | }, 175 | } 176 | 177 | impl QualifierQuery { 178 | pub fn qualifies_none(&self) -> bool { 179 | Qualifier::none().qualifies_as(self) 180 | } 181 | 182 | pub fn qualify(&self, qualifier: &Qualifier) -> bool { 183 | qualifier.qualifies_as(self) 184 | } 185 | } 186 | 187 | impl Default for QualifierQuery { 188 | fn default() -> Self { 189 | Self::Aggregate(Q::none()) 190 | } 191 | } 192 | 193 | impl QualifierQuery { 194 | pub fn none() -> Self { 195 | Self::Aggregate(Q::none()) 196 | } 197 | } 198 | 199 | impl From for QualifierQuery { 200 | fn from(value: Q) -> Self { 201 | QualifierQuery::Aggregate(value) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/querier.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::attribute::Attribute; 4 | use crate::plugin::GlobalStatRelations; 5 | use crate::stat::StatExt; 6 | use crate::{ 7 | plugin::GlobalStatDefaults, Buffer, QualifierFlag, QualifierQuery, Stat, StatInst, StatStream, 8 | }; 9 | use crate::{validate, StatValue, StatValuePair}; 10 | use bevy_ecs::reflect::ReflectComponent; 11 | use bevy_ecs::{ 12 | component::Component, 13 | entity::Entity, 14 | query::With, 15 | system::{Query, Res, SystemParam}, 16 | }; 17 | use bevy_reflect::Reflect; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | /// The core marker component. Stat querying is only allowed on entities marked as [`StatEntity`]. 21 | #[derive(Debug, Component, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Reflect)] 22 | #[reflect(Component)] 23 | pub struct StatEntity; 24 | 25 | /// A root [`SystemParam`] that curates all entities marked as [`StatEntity`]. 26 | /// 27 | /// Join with [`StatStream`]s via [`StatEntities::join`] to start querying. 28 | #[derive(Debug, SystemParam)] 29 | pub struct StatEntities<'w, 's, Q: QualifierFlag> { 30 | defaults: Option>, 31 | relations: Option>>, 32 | entities: Query<'w, 's, Entity, With>, 33 | } 34 | 35 | impl<'w, 's, Q: QualifierFlag> StatEntities<'w, 's, Q> { 36 | pub fn join<'t, S: StatStream>( 37 | &'t self, 38 | stream: S, 39 | ) -> JoinedQuerier<'w, 's, 't, Q, S> { 40 | JoinedQuerier { base: self, stream } 41 | } 42 | } 43 | 44 | pub struct JoinedQuerier<'w, 's, 't, Q: QualifierFlag, S: StatStream> { 45 | base: &'t StatEntities<'w, 's, Q>, 46 | stream: S, 47 | } 48 | 49 | impl<'w, 's, 't, Q: QualifierFlag, S: StatStream> JoinedQuerier<'w, 's, 't, Q, S> { 50 | pub fn join>( 51 | self, 52 | stream: T, 53 | ) -> JoinedQuerier<'w, 's, 't, Q, (S, T)> { 54 | JoinedQuerier { 55 | base: self.base, 56 | stream: (self.stream, stream), 57 | } 58 | } 59 | 60 | pub fn query_stat( 61 | &self, 62 | entity: Entity, 63 | qualifier: &QualifierQuery, 64 | stat: &T, 65 | ) -> Option { 66 | self.query_stat_erased(entity, qualifier, stat.as_entry()) 67 | .map(|x| unsafe { x.into() }) 68 | } 69 | 70 | pub fn query_relation( 71 | &self, 72 | from: Entity, 73 | to: Entity, 74 | qualifier: &QualifierQuery, 75 | stat: &T, 76 | ) -> Option { 77 | self.query_relation_erased(from, to, qualifier, stat.as_entry()) 78 | .map(|x| unsafe { x.into() }) 79 | } 80 | 81 | pub fn eval_stat( 82 | &self, 83 | entity: Entity, 84 | qualifier: &QualifierQuery, 85 | stat: &T, 86 | ) -> Option<::Out> { 87 | self.query_stat(entity, qualifier, stat).map(|x| x.eval()) 88 | } 89 | 90 | pub fn eval_relation( 91 | &self, 92 | from: Entity, 93 | to: Entity, 94 | qualifier: &QualifierQuery, 95 | stat: &T, 96 | ) -> Option<::Out> { 97 | self.query_relation(from, to, qualifier, stat) 98 | .map(|x| x.eval()) 99 | } 100 | 101 | pub fn has_attribute<'a>(&self, entity: Entity, attribute: impl Into>) -> bool { 102 | self.has_attribute_erased(entity, attribute.into()) 103 | } 104 | } 105 | 106 | impl> ErasedQuerier 107 | for JoinedQuerier<'_, '_, '_, Q, S> 108 | { 109 | fn query_stat_erased( 110 | &self, 111 | entity: Entity, 112 | query: &QualifierQuery, 113 | stat: StatInst, 114 | ) -> Option { 115 | let value = if let Some(defaults) = &self.base.defaults { 116 | defaults.get_dyn(stat) 117 | } else { 118 | (stat.vtable.default)() 119 | }; 120 | let mut pair = StatValuePair { stat, value }; 121 | if let Some(relations) = &self.base.relations { 122 | relations.stream_stat(entity, query, &mut pair, Querier(self)); 123 | } 124 | self.stream 125 | .stream_stat(entity, query, &mut pair, Querier(self)); 126 | Some(pair.value) 127 | } 128 | 129 | fn query_relation_erased( 130 | &self, 131 | from: Entity, 132 | to: Entity, 133 | query: &QualifierQuery, 134 | stat: StatInst, 135 | ) -> Option { 136 | let value = if let Some(defaults) = &self.base.defaults { 137 | defaults.get_dyn(stat) 138 | } else { 139 | (stat.vtable.default)() 140 | }; 141 | let mut pair = StatValuePair { stat, value }; 142 | self.stream 143 | .stream_relation(&self.stream, from, to, query, &mut pair, Querier(self)); 144 | Some(pair.value) 145 | } 146 | 147 | fn has_attribute_erased(&self, entity: Entity, attribute: Attribute) -> bool { 148 | self.stream.has_attribute(entity, attribute) 149 | } 150 | } 151 | 152 | /// An erased type that can query for stats on entities in the world. 153 | /// 154 | /// Notable implementors are [`NoopQuerier`] and [`JoinedQuerier`]. 155 | trait ErasedQuerier { 156 | /// Query for a stat in its component form. 157 | fn query_stat_erased( 158 | &self, 159 | entity: Entity, 160 | query: &QualifierQuery, 161 | stat: StatInst, 162 | ) -> Option; 163 | 164 | /// Query for a relation stat in its component form. 165 | fn query_relation_erased( 166 | &self, 167 | from: Entity, 168 | to: Entity, 169 | query: &QualifierQuery, 170 | stat: StatInst, 171 | ) -> Option; 172 | 173 | /// Query for the existence of a string attribute. 174 | fn has_attribute_erased(&self, entity: Entity, attribute: Attribute) -> bool; 175 | } 176 | 177 | /// An erased type that can query for stats on entities in the world. 178 | pub struct Querier<'t, Q: QualifierFlag>(&'t dyn ErasedQuerier); 179 | 180 | impl Clone for Querier<'_, Q> { 181 | fn clone(&self) -> Self { 182 | *self 183 | } 184 | } 185 | 186 | impl Copy for Querier<'_, Q> {} 187 | 188 | impl Debug for Querier<'_, Q> { 189 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 190 | f.debug_struct("Querier").finish_non_exhaustive() 191 | } 192 | } 193 | 194 | impl Querier<'_, Q> { 195 | /// Create a noop querier. 196 | pub fn noop() -> Querier<'static, Q> { 197 | static _Q: NoopQuerier = NoopQuerier; 198 | Querier(&_Q) 199 | } 200 | 201 | /// Query for a stat in its component form. 202 | pub fn query_stat( 203 | &self, 204 | entity: Entity, 205 | qualifier: &QualifierQuery, 206 | stat: &S, 207 | ) -> Option { 208 | validate::(); 209 | self.0 210 | .query_stat_erased(entity, qualifier, stat.as_entry()) 211 | .map(|x| unsafe { x.into() }) 212 | } 213 | 214 | /// Query for a relation stat in its component form. 215 | pub fn query_relation( 216 | &self, 217 | from: Entity, 218 | to: Entity, 219 | qualifier: &QualifierQuery, 220 | stat: &S, 221 | ) -> Option { 222 | validate::(); 223 | self.0 224 | .query_relation_erased(from, to, qualifier, stat.as_entry()) 225 | .map(|x| unsafe { x.into() }) 226 | } 227 | 228 | /// Query for a stat in its evaluated form. 229 | pub fn eval_stat( 230 | &self, 231 | entity: Entity, 232 | qualifier: &QualifierQuery, 233 | stat: &S, 234 | ) -> Option<::Out> { 235 | validate::(); 236 | self.query_stat(entity, qualifier, stat) 237 | .map(|x| StatValue::eval(&x)) 238 | } 239 | 240 | /// Query for a relation stat in its evaluated form. 241 | pub fn eval_relation( 242 | &self, 243 | from: Entity, 244 | to: Entity, 245 | qualifier: &QualifierQuery, 246 | stat: &S, 247 | ) -> Option<::Out> { 248 | validate::(); 249 | self.query_relation(from, to, qualifier, stat) 250 | .map(|x| StatValue::eval(&x)) 251 | } 252 | 253 | /// Query for the existence of an attribute. 254 | pub fn has_attribute<'a>(&self, entity: Entity, attribute: impl Into>) -> bool { 255 | self.0.has_attribute_erased(entity, attribute.into()) 256 | } 257 | } 258 | 259 | /// A [`Querier`] that does not provide the ability to query other entities. 260 | pub struct NoopQuerier; 261 | 262 | impl ErasedQuerier for NoopQuerier { 263 | fn query_relation_erased( 264 | &self, 265 | _: Entity, 266 | _: Entity, 267 | _: &QualifierQuery, 268 | _: StatInst, 269 | ) -> Option { 270 | None 271 | } 272 | 273 | fn query_stat_erased(&self, _: Entity, _: &QualifierQuery, _: StatInst) -> Option { 274 | None 275 | } 276 | 277 | fn has_attribute_erased(&self, _: Entity, _: Attribute) -> bool { 278 | false 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/rounding.rs: -------------------------------------------------------------------------------- 1 | use crate::Float; 2 | use bevy_reflect::TypePath; 3 | use std::fmt::Debug; 4 | 5 | /// Rounding method for a floating point number. 6 | pub trait Rounding: TypePath + Default + Debug + Copy + Send + Sync + 'static { 7 | /// Rounds to the an integer. 8 | fn round(input: F) -> F; 9 | } 10 | 11 | /// Rounds to 0. 12 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, TypePath)] 13 | pub struct Truncate; 14 | 15 | impl Rounding for Truncate { 16 | fn round(input: F) -> F { 17 | input.trunc() 18 | } 19 | } 20 | 21 | /// Rounds to the largest integer smaller than the float. 22 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, TypePath)] 23 | pub struct Floor; 24 | 25 | impl Rounding for Floor { 26 | fn round(input: F) -> F { 27 | input.floor() 28 | } 29 | } 30 | 31 | /// Rounds to the smallest integer larger than the float. 32 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, TypePath)] 33 | pub struct Ceil; 34 | 35 | impl Rounding for Ceil { 36 | fn round(input: F) -> F { 37 | input.ceil() 38 | } 39 | } 40 | 41 | /// Rounds to the nearest integer. 42 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, TypePath)] 43 | pub struct Round; 44 | 45 | impl Rounding for Round { 46 | fn round(input: F) -> F { 47 | input.round() 48 | } 49 | } 50 | 51 | /// Rounds `x > 0` to at least `1`, 52 | /// rounds `x < 0` to at most `-1`. 53 | /// rounds `x == 0` to `0`. 54 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, TypePath)] 55 | pub struct TruncateSigned; 56 | 57 | impl Rounding for TruncateSigned { 58 | fn round(input: F) -> F { 59 | if input > F::ZERO { 60 | input.trunc()._max(F::ONE) 61 | } else if input < F::ZERO { 62 | input.trunc()._min(F::ZERO - F::ONE) 63 | } else { 64 | input 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/stat.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | cmp::{Eq, Ord, Ordering}, 4 | fmt::Debug, 5 | hash::Hash, 6 | marker::PhantomData, 7 | ptr, 8 | }; 9 | 10 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 11 | 12 | use crate::{ 13 | cowstr::deserialize_cow_str, plugin::STAT_DESERIALIZERS, validate, Buffer, Shareable, StatValue, 14 | }; 15 | 16 | /// A `vtable` of dynamic functions on [`Stat::Value`]. 17 | #[repr(transparent)] 18 | pub struct StatVTable { 19 | vtable: ErasedStatVTable, 20 | p: PhantomData, 21 | } 22 | 23 | #[repr(C)] 24 | pub(crate) struct ErasedStatVTable { 25 | pub name: fn(u64) -> &'static str, 26 | pub join: unsafe fn(&mut Buffer, &Buffer), 27 | pub default: fn() -> Buffer, 28 | pub as_debug: unsafe fn(&Buffer) -> &dyn Debug, 29 | pub as_serialize: unsafe fn(&Buffer) -> &dyn erased_serde::Serialize, 30 | pub deserialize: fn(&mut dyn erased_serde::Deserializer) -> erased_serde::Result, 31 | pub clone: unsafe fn(&Buffer) -> Buffer, 32 | pub drop: unsafe fn(&mut Buffer), 33 | } 34 | 35 | impl StatVTable { 36 | /// Create a [`StatVTable`] of a given [`Stat`] type, complete with serialization support. 37 | pub const fn of>() -> StatVTable { 38 | StatVTable { 39 | vtable: ErasedStatVTable { 40 | name: |id| T::index_to_name(id), 41 | join: |to, from| { 42 | validate::(); 43 | let to = ptr::from_mut(to).cast::(); 44 | let from = ptr::from_ref(from).cast::(); 45 | unsafe { to.as_mut() } 46 | .unwrap() 47 | .join_by_ref(unsafe { from.as_ref().unwrap() }) 48 | }, 49 | default: || Buffer::from(T::Value::default()), 50 | as_debug: |buffer| unsafe { buffer.as_ref::() }, 51 | as_serialize: |buffer| unsafe { buffer.as_ref::() }, 52 | deserialize: |deserializer| { 53 | validate::(); 54 | let value: T::Value = erased_serde::deserialize(deserializer)?; 55 | Ok(Buffer::from(value)) 56 | }, 57 | clone: |buffer| Buffer::from(unsafe { buffer.as_ref::() }.clone()), 58 | drop: |buffer| { 59 | let value = unsafe { buffer.read_move::() }; 60 | drop(value) 61 | }, 62 | }, 63 | p: PhantomData, 64 | } 65 | } 66 | 67 | /// Create a [`StatVTable`] of a given [`Stat`] type, panics on serialization. 68 | pub const fn no_serialize() -> StatVTable { 69 | StatVTable { 70 | vtable: ErasedStatVTable { 71 | name: |id| T::index_to_name(id), 72 | join: |to, from| { 73 | validate::(); 74 | let to = ptr::from_mut(to).cast::(); 75 | let from = ptr::from_ref(from).cast::(); 76 | unsafe { to.as_mut() } 77 | .unwrap() 78 | .join_by_ref(unsafe { from.as_ref().unwrap() }) 79 | }, 80 | default: || Buffer::from(T::Value::default()), 81 | as_debug: |buffer| { 82 | validate::(); 83 | let ptr = ptr::from_ref(buffer).cast::(); 84 | unsafe { ptr.as_ref() }.unwrap() 85 | }, 86 | as_serialize: |_| panic!("Serialization is not supported."), 87 | deserialize: |_| panic!("Deserialization is not supported."), 88 | clone: |buffer| Buffer::from(unsafe { buffer.as_ref::() }.clone()), 89 | drop: |buffer| { 90 | let value = unsafe { buffer.read_move::() }; 91 | drop(value) 92 | }, 93 | }, 94 | p: PhantomData, 95 | } 96 | } 97 | } 98 | 99 | impl Debug for ErasedStatVTable { 100 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 101 | f.debug_struct("StatVTable").finish_non_exhaustive() 102 | } 103 | } 104 | 105 | fn ref_cmp(a: &T, b: &T) -> Ordering { 106 | (a as *const T as usize).cmp(&(b as *const T as usize)) 107 | } 108 | 109 | /// Instance of a stat. 110 | /// 111 | /// # Safety Invariant 112 | /// 113 | /// If two [`StatInst`]s are equal, their corresponding [`Stat`] and [`StatValue`] 114 | /// they are constructed from must be equal. 115 | /// This is achieved through the constraint placed on construction of [`StatVTable`], which makes 116 | /// having the same [`ErasedStatVTable`] on two different [`Stat`]s impossible in safe rust. 117 | #[derive(Debug, Clone, Copy)] 118 | pub struct StatInst { 119 | pub(crate) index: u64, 120 | pub(crate) vtable: &'static ErasedStatVTable, 121 | } 122 | 123 | impl StatInst { 124 | pub fn name(&self) -> &'static str { 125 | (self.vtable.name)(self.index) 126 | } 127 | 128 | pub unsafe fn clone_buffer(&self, buffer: &Buffer) -> Buffer { 129 | (self.vtable.clone)(buffer) 130 | } 131 | 132 | pub unsafe fn drop_buffer(&self, buffer: &mut Buffer) { 133 | (self.vtable.drop)(buffer) 134 | } 135 | } 136 | 137 | impl PartialEq for StatInst { 138 | fn eq(&self, other: &Self) -> bool { 139 | self.index == other.index && ptr::eq(self.vtable, other.vtable) 140 | } 141 | } 142 | 143 | impl Eq for StatInst {} 144 | 145 | impl PartialOrd for StatInst { 146 | fn partial_cmp(&self, other: &Self) -> Option { 147 | Some(self.cmp(other)) 148 | } 149 | } 150 | 151 | impl Ord for StatInst { 152 | fn cmp(&self, other: &Self) -> Ordering { 153 | ref_cmp(self.vtable, other.vtable).then(self.index.cmp(&other.index)) 154 | } 155 | } 156 | 157 | impl Hash for StatInst { 158 | fn hash(&self, state: &mut H) { 159 | self.index.hash(state); 160 | (ptr::from_ref(self.vtable) as usize).hash(state); 161 | } 162 | } 163 | 164 | /// Implement this on your types to qualify them as a [`Stat`]. 165 | /// 166 | /// Each implementor can have its own `Value` type so you may want multiple of them. 167 | pub trait Stat: Shareable { 168 | type Value: StatValue; 169 | 170 | /// Returns a globally unique name of the stat. 171 | fn name(&self) -> &'static str; 172 | 173 | /// Return a reference to a static [`StatVTable`] that supports `Debug`, `Drop` and serialization. 174 | /// 175 | /// # Example 176 | /// 177 | /// ``` 178 | /// fn vtable() -> &'static StatVTable{ 179 | /// static vtable: StatVTable = StatVTable::of::(); 180 | /// &vtable 181 | /// } 182 | /// ``` 183 | /// 184 | /// # Safety 185 | /// 186 | /// The resulting pointer must be unique across all implementors, this is achieved 187 | /// by having a generic constraint of `Self`. This can technically return multiple 188 | /// tables which would cause logical errors but not unsafety. 189 | fn vtable() -> &'static StatVTable; 190 | 191 | /// Returns a locally unique index of the stat, used in equality comparisons. 192 | fn as_index(&self) -> u64; 193 | 194 | /// Convert from a unique index of the stat. 195 | /// 196 | /// This function can panic in case of a mismatch. 197 | fn from_index(index: u64) -> Self; 198 | 199 | /// Register all fields for serialization. 200 | fn values() -> impl IntoIterator; 201 | 202 | /// Check for equality on generic stats. 203 | fn is(&self, other: &T) -> bool { 204 | self.as_entry() == other.as_entry() 205 | } 206 | } 207 | 208 | /// Extension methods to [`Stat`]. 209 | pub(crate) trait StatExt: Stat { 210 | fn index_to_name(index: u64) -> &'static str { 211 | Self::from_index(index).name() 212 | } 213 | 214 | fn as_entry(&self) -> StatInst { 215 | StatInst { 216 | index: self.as_index(), 217 | vtable: &Self::vtable().vtable, 218 | } 219 | } 220 | 221 | /// Cast a generic [`Stat::Value`] to a concrete one. 222 | fn cast<'t, T: Stat>(&self, value: &'t mut Self::Value) -> Option<(&T, &'t mut T::Value)> { 223 | if TypeId::of::() == TypeId::of::() { 224 | Some(( 225 | (self as &dyn Any).downcast_ref()?, 226 | (value as &mut dyn Any).downcast_mut()?, 227 | )) 228 | } else { 229 | None 230 | } 231 | } 232 | 233 | /// Cast a generic [`Stat::Value`] to a concrete one if stat is equal. 234 | fn is_then_cast<'t, T: Stat>( 235 | &self, 236 | other: &T, 237 | value: &'t mut Self::Value, 238 | ) -> Option<&'t mut T::Value> { 239 | if !self.is(other) { 240 | return None; 241 | } 242 | (value as &mut dyn Any).downcast_mut() 243 | } 244 | } 245 | 246 | impl StatExt for T where T: Stat {} 247 | 248 | impl Serialize for StatInst { 249 | fn serialize(&self, serializer: S) -> Result 250 | where 251 | S: serde::Serializer, 252 | { 253 | (self.vtable.name)(self.index).serialize(serializer) 254 | } 255 | } 256 | 257 | impl<'de> Deserialize<'de> for StatInst { 258 | fn deserialize(deserializer: D) -> Result 259 | where 260 | D: serde::Deserializer<'de>, 261 | { 262 | let s = deserialize_cow_str(deserializer)?; 263 | if !STAT_DESERIALIZERS.is_set() { 264 | return Err(serde::de::Error::custom( 265 | "Must add context `STAT_DESERIALIZERS` before deserializing stat.", 266 | )); 267 | } 268 | STAT_DESERIALIZERS.with(|cx| { 269 | if let Some(result) = cx.concrete.get(s.as_ref()) { 270 | Ok(*result) 271 | } else { 272 | Err(serde::de::Error::custom(format!( 273 | "Unable to parse Stat \"{s}\"." 274 | ))) 275 | } 276 | }) 277 | } 278 | } 279 | 280 | /// A pair of stat and value in a query. 281 | /// 282 | /// # Safety Invariant 283 | /// `value` must be the correct [`Stat::Value`]. 284 | pub struct StatValuePair { 285 | pub(crate) stat: StatInst, 286 | pub(crate) value: Buffer, 287 | } 288 | 289 | impl Debug for StatValuePair { 290 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 291 | f.debug_struct("StatValuePair") 292 | .field("stat", &self.stat.name()) 293 | .field("value", unsafe { 294 | &(self.stat.vtable.as_debug)(&self.value) 295 | }) 296 | .finish() 297 | } 298 | } 299 | 300 | impl StatValuePair { 301 | pub fn new(stat: &S, value: S::Value) -> Self { 302 | StatValuePair { 303 | stat: stat.as_entry(), 304 | value: Buffer::from(value), 305 | } 306 | } 307 | 308 | pub fn new_default(stat: &S) -> Self { 309 | StatValuePair { 310 | stat: stat.as_entry(), 311 | value: Buffer::from(S::Value::default()), 312 | } 313 | } 314 | 315 | /// Check for equality on generic stats. 316 | pub fn is(&self, other: &T) -> bool { 317 | self.stat == other.as_entry() 318 | } 319 | 320 | /// Cast to a concrete [`Stat::Value`]. 321 | pub fn cast<'t, T: Stat>(&mut self) -> Option<(T, &'t mut T::Value)> { 322 | validate::(); 323 | if ptr::eq(self.stat.vtable, &T::vtable().vtable) { 324 | let ptr = ptr::from_mut(&mut self.value) as *mut T::Value; 325 | Some(( 326 | T::from_index(self.stat.index), 327 | unsafe { ptr.as_mut() }.unwrap(), 328 | )) 329 | } else { 330 | None 331 | } 332 | } 333 | 334 | /// Cast to a concrete [`Stat::Value`]. 335 | pub fn is_then_cast<'t, T: Stat>(&mut self, other: &T) -> Option<&'t mut T::Value> { 336 | validate::(); 337 | if self.stat == other.as_entry() { 338 | let ptr = ptr::from_mut(&mut self.value) as *mut T::Value; 339 | unsafe { ptr.as_mut() } 340 | } else { 341 | None 342 | } 343 | } 344 | 345 | /// Cast to a concrete [`Stat::Value`]. 346 | pub fn into_result(self) -> Option { 347 | validate::(); 348 | if ptr::eq(self.stat.vtable, &T::vtable().vtable) { 349 | Some(unsafe { self.value.into() }) 350 | } else { 351 | None 352 | } 353 | } 354 | 355 | pub(crate) fn clone_buffer(&self) -> Buffer { 356 | // Safety: Safe because invariant. 357 | unsafe { self.stat.clone_buffer(&self.value) } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/stat_map.rs: -------------------------------------------------------------------------------- 1 | use crate::operations::StatOperation; 2 | use crate::stat::StatValuePair; 3 | use crate::{ 4 | Buffer, Qualifier, QualifierFlag, QualifierQuery, Querier, Stat, StatExt, StatInst, StatStream, 5 | StatValue, 6 | }; 7 | use bevy_ecs::component::Component; 8 | use bevy_ecs::entity::Entity; 9 | use bevy_ecs::reflect::ReflectComponent; 10 | use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; 11 | use serde::de::{DeserializeOwned, DeserializeSeed, Visitor}; 12 | use serde::ser::SerializeSeq; 13 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 14 | use std::cmp::Ordering; 15 | use std::fmt::Debug; 16 | use std::marker::PhantomData; 17 | use std::mem; 18 | 19 | pub(crate) struct StatMapEntry { 20 | stat: StatInst, 21 | qualifier: Qualifier, 22 | buffer: Buffer, 23 | } 24 | 25 | impl Clone for StatMapEntry { 26 | fn clone(&self) -> Self { 27 | Self { 28 | stat: self.stat, 29 | qualifier: self.qualifier.clone(), 30 | buffer: unsafe { self.stat.clone_buffer(&self.buffer) }, 31 | } 32 | } 33 | } 34 | 35 | impl Drop for StatMapEntry { 36 | fn drop(&mut self) { 37 | unsafe { (self.stat.vtable.drop)(&mut self.buffer) }; 38 | } 39 | } 40 | 41 | impl StatMapEntry { 42 | /// # Safety 43 | /// 44 | /// `T` must be the stored type. 45 | unsafe fn take(mut self) -> T { 46 | let result = self.buffer.read_move(); 47 | mem::forget(self); 48 | result 49 | } 50 | } 51 | 52 | /// A type erased storage component of qualified stats. 53 | /// 54 | /// This type can hold any qualifier stat combination as long as the qualifier type is the same. 55 | /// 56 | /// # Performance 57 | /// 58 | /// The type is intended to hold relatively constant items and prioritizes querying, 59 | /// not optimized for rapid insertion or removal. 60 | /// 61 | /// # Serialization 62 | /// 63 | /// Deserialization must be done in a [`STAT_DESERIALIZERS`](crate::STAT_DESERIALIZERS) thread local scope. 64 | /// This can be seamlessly integrated with the `bevy_serde_lens` crate. 65 | #[derive(Component, Serialize, Deserialize, Reflect, Clone)] 66 | #[reflect(Component, Serialize, Deserialize)] 67 | #[reflect(where Q: Serialize + DeserializeOwned)] 68 | pub struct StatMap { 69 | #[reflect(ignore)] 70 | inner: Vec>, 71 | } 72 | 73 | impl Debug for StatMap { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 75 | #[derive(Debug)] 76 | struct Stat(&'static str); 77 | let mut map = f.debug_map(); 78 | for StatMapEntry { 79 | stat, 80 | qualifier, 81 | buffer, 82 | } in &self.inner 83 | { 84 | map.entry(&(qualifier, Stat(stat.name())), unsafe { 85 | (stat.vtable.as_debug)(buffer) 86 | }); 87 | } 88 | map.finish() 89 | } 90 | } 91 | 92 | impl Default for StatMap { 93 | fn default() -> Self { 94 | StatMap { inner: Vec::new() } 95 | } 96 | } 97 | 98 | fn sort(a: &StatMapEntry, b: &StatMapEntry) -> Ordering { 99 | a.stat.cmp(&b.stat).then(a.qualifier.cmp(&b.qualifier)) 100 | } 101 | 102 | impl FromIterator<(Qualifier, S, S::Value)> for StatMap { 103 | fn from_iter, S, S::Value)>>(iter: T) -> Self { 104 | let mut inner: Vec<_> = iter 105 | .into_iter() 106 | .map(|(qualifier, stat, value)| { 107 | let stat = stat.as_entry(); 108 | StatMapEntry { 109 | stat, 110 | qualifier, 111 | buffer: Buffer::from(value), 112 | } 113 | }) 114 | .collect(); 115 | inner.sort_by(sort); 116 | StatMap { inner } 117 | } 118 | } 119 | 120 | impl Extend<(Qualifier, S, S::Value)> for StatMap { 121 | fn extend, S, S::Value)>>(&mut self, iter: T) { 122 | self.inner 123 | .extend(iter.into_iter().map(|(qualifier, stat, value)| { 124 | let stat = stat.as_entry(); 125 | StatMapEntry { 126 | stat, 127 | qualifier, 128 | buffer: Buffer::from(value), 129 | } 130 | })); 131 | self.inner.sort_by(sort); 132 | } 133 | } 134 | 135 | impl StatMap { 136 | pub const fn new() -> Self { 137 | Self { inner: Vec::new() } 138 | } 139 | 140 | /// Drops all items in the map. 141 | pub fn clear(&mut self) { 142 | self.inner.clear() 143 | } 144 | 145 | /// Returns true if the map contains no elements. 146 | pub fn is_empty(&self) -> bool { 147 | self.inner.is_empty() 148 | } 149 | 150 | /// Returns the number of elements in the map. 151 | pub fn len(&self) -> usize { 152 | self.inner.len() 153 | } 154 | 155 | /// Performs a binary search for a value. 156 | fn binary_search(&self, qualifier: &Qualifier, stat: &StatInst) -> Result { 157 | self.inner.binary_search_by( 158 | |StatMapEntry { 159 | stat: s, 160 | qualifier: q, 161 | buffer: _, 162 | }| { (s, q).cmp(&(stat, qualifier)) }, 163 | ) 164 | } 165 | 166 | /// Inserts a [`Stat::Value`] in its component form. 167 | pub fn insert(&mut self, qualifier: Qualifier, stat: S, value: S::Value) { 168 | let stat = stat.as_entry(); 169 | let buffer = Buffer::from(value); 170 | match self.binary_search(&qualifier, &stat) { 171 | Ok(at) => self.inner[at].buffer = buffer, 172 | Err(at) => self.inner.insert( 173 | at, 174 | StatMapEntry { 175 | stat, 176 | qualifier, 177 | buffer, 178 | }, 179 | ), 180 | }; 181 | } 182 | 183 | /// Inserts a [`Stat::Value`] in its evaluated form. 184 | pub fn insert_base( 185 | &mut self, 186 | qualifier: Qualifier, 187 | stat: S, 188 | base: ::Base, 189 | ) { 190 | let stat = stat.as_entry(); 191 | let buffer = Buffer::from(S::Value::from_base(base)); 192 | match self.binary_search(&qualifier, &stat) { 193 | Ok(at) => self.inner[at].buffer = buffer, 194 | Err(at) => self.inner.insert( 195 | at, 196 | StatMapEntry { 197 | stat, 198 | qualifier, 199 | buffer, 200 | }, 201 | ), 202 | }; 203 | } 204 | 205 | /// Obtains a [`Stat::Value`]. 206 | pub fn get(&self, qualifier: &Qualifier, stat: &S) -> Option<&S::Value> { 207 | let stat = stat.as_entry(); 208 | match self.binary_search(qualifier, &stat) { 209 | Ok(at) => Some(unsafe { self.inner[at].buffer.as_ref() }), 210 | Err(_) => None, 211 | } 212 | } 213 | 214 | /// Obtains a mutable [`Stat::Value`]. 215 | pub fn get_mut( 216 | &mut self, 217 | qualifier: &Qualifier, 218 | stat: &S, 219 | ) -> Option<&mut S::Value> { 220 | let stat = stat.as_entry(); 221 | match self.binary_search(qualifier, &stat) { 222 | Ok(at) => Some(unsafe { self.inner[at].buffer.as_mut() }), 223 | Err(_) => None, 224 | } 225 | } 226 | 227 | /// Removes and obtains a [`Stat::Value`]. 228 | pub fn remove(&mut self, qualifier: &Qualifier, stat: &S) -> Option { 229 | let stat = stat.as_entry(); 230 | match self.binary_search(qualifier, &stat) { 231 | Ok(at) => Some(unsafe { self.inner.remove(at).take() }), 232 | Err(_) => None, 233 | } 234 | } 235 | 236 | /// Obtains a [`Stat::Value`] in its evaluated form. 237 | pub fn get_evaled( 238 | &self, 239 | qualifier: &Qualifier, 240 | stat: &S, 241 | ) -> Option<::Out> { 242 | let stat = stat.as_entry(); 243 | match self.binary_search(qualifier, &stat) { 244 | Ok(at) => Some(unsafe { self.inner[at].buffer.as_ref::().eval() }), 245 | Err(_) => None, 246 | } 247 | } 248 | 249 | /// Iterate over a particular stat. 250 | pub(crate) fn slice(&self, stat: StatInst) -> &[StatMapEntry] { 251 | let fst = self.inner.partition_point(|x| x.stat < stat); 252 | let snd = self.inner.partition_point(|x| x.stat <= stat); 253 | &self.inner[fst..snd] 254 | } 255 | 256 | /// Iterate over a particular stat. 257 | pub(crate) fn slice_mut(&mut self, stat: StatInst) -> &mut [StatMapEntry] { 258 | let fst = self.inner.partition_point(|x| x.stat < stat); 259 | let snd = self.inner.partition_point(|x| x.stat <= stat); 260 | &mut self.inner[fst..snd] 261 | } 262 | 263 | /// Iterate over a particular stat. 264 | pub fn iter(&self, stat: &S) -> impl Iterator, &S::Value)> { 265 | let stat = stat.as_entry(); 266 | self.slice(stat) 267 | .iter() 268 | .map(|x| (&x.qualifier, unsafe { x.buffer.as_ref() })) 269 | } 270 | 271 | /// Iterate over a particular stat. 272 | pub fn iter_mut( 273 | &mut self, 274 | stat: &S, 275 | ) -> impl Iterator, &mut S::Value)> { 276 | let stat = stat.as_entry(); 277 | self.slice_mut(stat) 278 | .iter_mut() 279 | .map(|x| (&x.qualifier, unsafe { x.buffer.as_mut() })) 280 | } 281 | 282 | /// Remove all instances of a given stat. 283 | pub fn remove_all(&mut self, stat: &S) { 284 | let stat = stat.as_entry(); 285 | let fst = self.inner.partition_point(|x| x.stat < stat); 286 | let snd = self.inner.partition_point(|x| x.stat <= stat); 287 | self.inner.drain(fst..snd); 288 | } 289 | 290 | /// Create or modify a stat via a [`StatOperation`]. 291 | /// 292 | /// Create a [`Default`] stat if non-existent. 293 | pub fn modify( 294 | &mut self, 295 | qualifier: Qualifier, 296 | stat: S, 297 | value: StatOperation, 298 | ) { 299 | let stat = stat.as_entry(); 300 | match self.binary_search(&qualifier, &stat) { 301 | Ok(at) => value.write_to(unsafe { self.inner[at].buffer.as_mut() }), 302 | Err(at) => { 303 | let buffer = Buffer::from(value.into_stat()); 304 | self.inner.insert( 305 | at, 306 | StatMapEntry { 307 | stat, 308 | qualifier, 309 | buffer, 310 | }, 311 | ); 312 | } 313 | } 314 | } 315 | 316 | /// Create or modify a stat via a closure. 317 | /// 318 | /// Create a [`Default`] stat if non-existent. 319 | pub fn modify_with( 320 | &mut self, 321 | qualifier: Qualifier, 322 | stat: &S, 323 | f: impl FnOnce(&mut S::Value), 324 | ) { 325 | let stat = stat.as_entry(); 326 | match self.binary_search(&qualifier, &stat) { 327 | Ok(at) => f(unsafe { self.inner[at].buffer.as_mut() }), 328 | Err(at) => { 329 | let mut value = Default::default(); 330 | f(&mut value); 331 | let buffer = Buffer::from(value); 332 | self.inner.insert( 333 | at, 334 | StatMapEntry { 335 | stat, 336 | qualifier, 337 | buffer, 338 | }, 339 | ); 340 | } 341 | } 342 | } 343 | 344 | pub fn query_stat(&self, qualifier: &QualifierQuery, stat: &S) -> S::Value { 345 | let mut stat = StatValuePair::new_default(stat); 346 | self.stream_stat(Entity::PLACEHOLDER, qualifier, &mut stat, Querier::noop()); 347 | unsafe { stat.value.into::() } 348 | } 349 | 350 | pub fn eval_stat( 351 | &self, 352 | qualifier: &QualifierQuery, 353 | stat: &S, 354 | ) -> ::Out { 355 | self.query_stat(qualifier, stat).eval() 356 | } 357 | } 358 | 359 | impl StatStream for StatMap { 360 | type Qualifier = Q; 361 | 362 | fn stream_stat( 363 | &self, 364 | _: Entity, 365 | qualifier: &crate::QualifierQuery, 366 | stat_value: &mut StatValuePair, 367 | _: Querier, 368 | ) { 369 | let f = stat_value.stat.vtable.join; 370 | for entry in self.slice(stat_value.stat) { 371 | if entry.qualifier.qualifies_as(qualifier) { 372 | unsafe { f(&mut stat_value.value, &entry.buffer) }; 373 | } 374 | } 375 | } 376 | } 377 | 378 | impl Serialize for StatMapEntry { 379 | fn serialize(&self, serializer: S) -> Result { 380 | let mut seq = serializer.serialize_seq(Some(3))?; 381 | seq.serialize_element(&self.qualifier)?; 382 | seq.serialize_element(&self.stat.name())?; 383 | seq.serialize_element(unsafe { &(self.stat.vtable.as_serialize)(&self.buffer) })?; 384 | seq.end() 385 | } 386 | } 387 | 388 | impl<'de, Q: QualifierFlag + Deserialize<'de>> Deserialize<'de> for StatMapEntry { 389 | fn deserialize>(deserializer: D) -> Result { 390 | let (qualifier, stat, buffer) = 391 | deserializer.deserialize_seq(TupleSeed::(PhantomData))?; 392 | Ok(StatMapEntry { 393 | stat, 394 | qualifier, 395 | buffer, 396 | }) 397 | } 398 | } 399 | 400 | pub struct TupleSeed(PhantomData); 401 | 402 | pub struct DynSeed { 403 | f: fn(&mut dyn erased_serde::Deserializer) -> erased_serde::Result, 404 | q: PhantomData, 405 | } 406 | 407 | impl<'de, Q: QualifierFlag + Deserialize<'de>> DeserializeSeed<'de> for TupleSeed { 408 | type Value = (Qualifier, StatInst, Buffer); 409 | 410 | fn deserialize(self, deserializer: D) -> Result 411 | where 412 | D: serde::Deserializer<'de>, 413 | { 414 | deserializer.deserialize_seq(TupleSeed::(PhantomData)) 415 | } 416 | } 417 | 418 | impl<'de, Q: QualifierFlag + Deserialize<'de>> Visitor<'de> for TupleSeed { 419 | type Value = (Qualifier, StatInst, Buffer); 420 | 421 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 422 | formatter.write_str("(qualifier, stat, value)") 423 | } 424 | 425 | fn visit_seq(self, mut seq: A) -> Result 426 | where 427 | A: serde::de::SeqAccess<'de>, 428 | { 429 | let Some(qualifier) = seq.next_element()? else { 430 | return Err(serde::de::Error::custom("Expected qualifier.")); 431 | }; 432 | let Some(stat) = seq.next_element::()? else { 433 | return Err(serde::de::Error::custom("Expected stat name.")); 434 | }; 435 | let Some(buffer) = seq.next_element_seed(DynSeed { 436 | f: stat.vtable.deserialize, 437 | q: PhantomData::, 438 | })? 439 | else { 440 | return Err(serde::de::Error::custom("Expected stat value.")); 441 | }; 442 | Ok((qualifier, stat, buffer)) 443 | } 444 | } 445 | 446 | impl<'de, Q: QualifierFlag> DeserializeSeed<'de> for DynSeed { 447 | type Value = Buffer; 448 | 449 | fn deserialize>(self, deserializer: D) -> Result { 450 | let deserializer = &mut ::erase(deserializer); 451 | (self.f)(deserializer).map_err(serde::de::Error::custom) 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{attribute::Attribute, stat::StatValuePair, QualifierFlag, QualifierQuery, Querier}; 2 | #[allow(unused)] 3 | use bevy_ecs::component::Component; 4 | use bevy_ecs::{ 5 | component::Mutable, 6 | entity::Entity, 7 | hierarchy::Children, 8 | query::QueryData, 9 | relationship::RelationshipTarget, 10 | system::{Query, StaticSystemParam, SystemParam}, 11 | }; 12 | 13 | /// An isolated item that provides stat modifiers to a stat query. 14 | #[allow(unused_variables)] 15 | pub trait StatStream { 16 | type Qualifier: QualifierFlag; 17 | 18 | fn stream_stat( 19 | &self, 20 | entity: Entity, 21 | qualifier: &QualifierQuery, 22 | stat_value: &mut StatValuePair, 23 | querier: Querier, 24 | ) { 25 | } 26 | 27 | fn stream_relation( 28 | &self, 29 | other: &Self, 30 | entity: Entity, 31 | target: Entity, 32 | qualifier: &QualifierQuery, 33 | stat_value: &mut StatValuePair, 34 | querier: Querier, 35 | ) { 36 | } 37 | 38 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 39 | false 40 | } 41 | } 42 | 43 | impl StatStream for &T 44 | where 45 | T: StatStream, 46 | { 47 | type Qualifier = T::Qualifier; 48 | 49 | fn stream_stat( 50 | &self, 51 | entity: Entity, 52 | qualifier: &QualifierQuery, 53 | stat_value: &mut StatValuePair, 54 | querier: Querier, 55 | ) { 56 | T::stream_stat(self, entity, qualifier, stat_value, querier); 57 | } 58 | 59 | fn stream_relation( 60 | &self, 61 | other: &Self, 62 | entity: Entity, 63 | target: Entity, 64 | qualifier: &QualifierQuery, 65 | stat_value: &mut StatValuePair, 66 | querier: Querier, 67 | ) { 68 | T::stream_relation(self, other, entity, target, qualifier, stat_value, querier); 69 | } 70 | 71 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 72 | T::has_attribute(self, entity, attribute) 73 | } 74 | } 75 | 76 | impl StatStream for (A, B) 77 | where 78 | A: StatStream, 79 | B: StatStream, 80 | { 81 | type Qualifier = A::Qualifier; 82 | 83 | fn stream_stat( 84 | &self, 85 | entity: Entity, 86 | qualifier: &QualifierQuery, 87 | stat_value: &mut StatValuePair, 88 | querier: Querier, 89 | ) { 90 | self.0.stream_stat(entity, qualifier, stat_value, querier); 91 | self.1.stream_stat(entity, qualifier, stat_value, querier); 92 | } 93 | 94 | fn stream_relation( 95 | &self, 96 | other: &Self, 97 | entity: Entity, 98 | target: Entity, 99 | qualifier: &QualifierQuery, 100 | stat_value: &mut StatValuePair, 101 | querier: Querier, 102 | ) { 103 | self.0 104 | .stream_relation(&other.0, entity, target, qualifier, stat_value, querier); 105 | self.1 106 | .stream_relation(&other.1, entity, target, qualifier, stat_value, querier); 107 | } 108 | 109 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 110 | self.0.has_attribute(entity, attribute) || self.1.has_attribute(entity, attribute) 111 | } 112 | } 113 | 114 | /// A set of [`Component`]s and external [`SystemParam`]s that provide 115 | /// stat modifiers for an [`Entity`]. 116 | #[allow(unused_variables)] 117 | pub trait QueryStream: 'static { 118 | type Qualifier: QualifierFlag; 119 | type Query: QueryData + 'static; 120 | type Context: SystemParam + 'static; 121 | 122 | fn stream_stat( 123 | query: <::ReadOnly as QueryData>::Item<'_>, 124 | context: &::Item<'_, '_>, 125 | entity: Entity, 126 | qualifier: &QualifierQuery, 127 | stat_value: &mut StatValuePair, 128 | querier: Querier, 129 | ) { 130 | } 131 | 132 | fn stream_relation( 133 | this: <::ReadOnly as QueryData>::Item<'_>, 134 | other: <::ReadOnly as QueryData>::Item<'_>, 135 | context: &::Item<'_, '_>, 136 | entity: Entity, 137 | target: Entity, 138 | qualifier: &QualifierQuery, 139 | stat_value: &mut StatValuePair, 140 | querier: Querier, 141 | ) { 142 | } 143 | 144 | fn has_attribute( 145 | query: <::ReadOnly as QueryData>::Item<'_>, 146 | context: &::Item<'_, '_>, 147 | entity: Entity, 148 | attribute: Attribute, 149 | ) -> bool { 150 | false 151 | } 152 | } 153 | 154 | impl QueryStream for T 155 | where 156 | T: Component + StatStream, 157 | { 158 | type Qualifier = T::Qualifier; 159 | type Query = &'static mut T; 160 | type Context = (); 161 | 162 | fn stream_stat( 163 | query: &T, 164 | _: &(), 165 | entity: Entity, 166 | qualifier: &QualifierQuery, 167 | stat_value: &mut StatValuePair, 168 | querier: Querier, 169 | ) { 170 | query.stream_stat(entity, qualifier, stat_value, querier); 171 | } 172 | 173 | fn stream_relation( 174 | this: &T, 175 | other: &T, 176 | _: &(), 177 | entity: Entity, 178 | target: Entity, 179 | qualifier: &QualifierQuery, 180 | stat_value: &mut StatValuePair, 181 | querier: Querier, 182 | ) { 183 | this.stream_relation(other, entity, target, qualifier, stat_value, querier); 184 | } 185 | 186 | fn has_attribute(query: &T, _: &(), entity: Entity, attribute: Attribute) -> bool { 187 | query.has_attribute(entity, attribute) 188 | } 189 | } 190 | 191 | /// [`SystemParam`] for querying a [`QueryStream`]. 192 | #[derive(SystemParam)] 193 | pub struct StatQuery<'w, 's, T: QueryStream> { 194 | pub query: Query<'w, 's, <::Query as QueryData>::ReadOnly>, 195 | pub context: StaticSystemParam<'w, 's, ::Context>, 196 | } 197 | 198 | /// [`SystemParam`] for querying a [`QueryStream`]. 199 | /// 200 | /// Unlike [`StatQuery`], [`StatQueryMut`]'s query portion uses a mutable query, 201 | /// this is useful if you want to query and modify at the same time. 202 | #[derive(SystemParam)] 203 | pub struct StatQueryMut<'w, 's, T: QueryStream> { 204 | pub query: Query<'w, 's, ::Query>, 205 | pub context: StaticSystemParam<'w, 's, ::Context>, 206 | } 207 | 208 | impl StatStream for StatQuery<'_, '_, T> { 209 | type Qualifier = T::Qualifier; 210 | 211 | fn stream_stat( 212 | &self, 213 | entity: Entity, 214 | qualifier: &QualifierQuery, 215 | stat_value: &mut StatValuePair, 216 | querier: Querier, 217 | ) { 218 | if let Ok(item) = self.query.get(entity) { 219 | T::stream_stat(item, &self.context, entity, qualifier, stat_value, querier); 220 | } 221 | } 222 | 223 | fn stream_relation( 224 | &self, 225 | _: &Self, 226 | entity: Entity, 227 | target: Entity, 228 | qualifier: &QualifierQuery, 229 | stat_value: &mut StatValuePair, 230 | querier: Querier, 231 | ) { 232 | if let Ok([this, other]) = self.query.get_many([entity, target]) { 233 | T::stream_relation( 234 | this, 235 | other, 236 | &self.context, 237 | entity, 238 | target, 239 | qualifier, 240 | stat_value, 241 | querier, 242 | ); 243 | } 244 | } 245 | 246 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 247 | if let Ok(item) = self.query.get(entity) { 248 | T::has_attribute(item, &self.context, entity, attribute) 249 | } else { 250 | false 251 | } 252 | } 253 | } 254 | 255 | impl StatStream for StatQueryMut<'_, '_, T> { 256 | type Qualifier = T::Qualifier; 257 | 258 | fn stream_stat( 259 | &self, 260 | entity: Entity, 261 | qualifier: &QualifierQuery, 262 | stat_value: &mut StatValuePair, 263 | querier: Querier, 264 | ) { 265 | if let Ok(item) = self.query.get(entity) { 266 | T::stream_stat(item, &self.context, entity, qualifier, stat_value, querier); 267 | } 268 | } 269 | 270 | fn stream_relation( 271 | &self, 272 | _: &Self, 273 | entity: Entity, 274 | target: Entity, 275 | qualifier: &QualifierQuery, 276 | stat_value: &mut StatValuePair, 277 | querier: Querier, 278 | ) { 279 | if let Ok([this, other]) = self.query.get_many([entity, target]) { 280 | T::stream_relation( 281 | this, 282 | other, 283 | &self.context, 284 | entity, 285 | target, 286 | qualifier, 287 | stat_value, 288 | querier, 289 | ); 290 | } 291 | } 292 | 293 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 294 | if let Ok(item) = self.query.get(entity) { 295 | T::has_attribute(item, &self.context, entity, attribute) 296 | } else { 297 | false 298 | } 299 | } 300 | } 301 | 302 | /// A component that references other entities, like [`Children`]. 303 | pub trait EntityReference: Component + 'static { 304 | fn iter_entities(&self) -> impl Iterator; 305 | } 306 | 307 | impl EntityReference for T 308 | where 309 | T: RelationshipTarget, 310 | { 311 | fn iter_entities(&self) -> impl Iterator { 312 | self.iter() 313 | } 314 | } 315 | 316 | /// [`SystemParam`] for querying [`QueryStream`]s on entities referenced by a component like [`Children`]. 317 | /// 318 | /// `query_relation` implementation is disabled since the behavior is undefined. 319 | #[derive(SystemParam)] 320 | pub struct ChildQuery<'w, 's, T: QueryStream, C: EntityReference = Children> { 321 | pub query: Query<'w, 's, <::Query as QueryData>::ReadOnly>, 322 | pub context: StaticSystemParam<'w, 's, ::Context>, 323 | pub children: Query<'w, 's, &'static C>, 324 | } 325 | 326 | /// [`SystemParam`] for querying [`QueryStream`]s on entities referenced by a component like [`Children`]. 327 | /// 328 | /// `query_relation` implementation is disabled since the behavior is undefined. 329 | #[derive(SystemParam)] 330 | pub struct ChildQueryMut<'w, 's, T: QueryStream, C: EntityReference = Children> { 331 | pub query: Query<'w, 's, ::Query>, 332 | pub context: StaticSystemParam<'w, 's, ::Context>, 333 | pub children: Query<'w, 's, &'static C>, 334 | } 335 | 336 | impl StatStream for ChildQuery<'_, '_, T, C> { 337 | type Qualifier = T::Qualifier; 338 | 339 | fn stream_stat( 340 | &self, 341 | entity: Entity, 342 | qualifier: &QualifierQuery, 343 | stat_value: &mut StatValuePair, 344 | querier: Querier, 345 | ) { 346 | if let Ok(children) = self.children.get(entity) { 347 | for item in self.query.iter_many(children.iter_entities()) { 348 | T::stream_stat(item, &self.context, entity, qualifier, stat_value, querier); 349 | } 350 | } 351 | } 352 | 353 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 354 | if let Ok(children) = self.children.get(entity) { 355 | for item in self.query.iter_many(children.iter_entities()) { 356 | if T::has_attribute(item, &self.context, entity, attribute) { 357 | return true; 358 | } 359 | } 360 | } 361 | false 362 | } 363 | } 364 | 365 | impl StatStream for ChildQueryMut<'_, '_, T, C> { 366 | type Qualifier = T::Qualifier; 367 | 368 | fn stream_stat( 369 | &self, 370 | entity: Entity, 371 | qualifier: &QualifierQuery, 372 | stat_value: &mut StatValuePair, 373 | querier: Querier, 374 | ) { 375 | if let Ok(children) = self.children.get(entity) { 376 | for item in self.query.iter_many(children.iter_entities()) { 377 | T::stream_stat(item, &self.context, entity, qualifier, stat_value, querier); 378 | } 379 | } 380 | } 381 | 382 | fn has_attribute(&self, entity: Entity, attribute: Attribute) -> bool { 383 | if let Ok(children) = self.children.get(entity) { 384 | for item in self.query.iter_many(children.iter_entities()) { 385 | if T::has_attribute(item, &self.context, entity, attribute) { 386 | return true; 387 | } 388 | } 389 | } 390 | false 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/types/flags.rs: -------------------------------------------------------------------------------- 1 | use crate::num_traits::Flags; 2 | use bevy_reflect::TypePath; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{fmt::Debug, mem, ops::BitAnd}; 5 | 6 | use crate::{operations::Unsupported, StatValue}; 7 | 8 | /// A flags based on a type that supports bitwise operations, 9 | /// like integer, `bitflgs` or `enumset`. 10 | #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, TypePath)] 11 | #[repr(transparent)] 12 | pub struct StatFlags(T); 13 | 14 | impl StatFlags { 15 | pub const fn new(item: T) -> Self { 16 | StatFlags(item) 17 | } 18 | 19 | pub fn exclude(&mut self, item: T) { 20 | let this = mem::take(self); 21 | self.0 = this.0.exclude(item); 22 | } 23 | 24 | pub fn contains(&self, item: T) -> bool 25 | where 26 | T: BitAnd + PartialEq, 27 | { 28 | self.0.clone() & item == self.0 29 | } 30 | } 31 | 32 | impl StatValue for StatFlags { 33 | type Out = T; 34 | type Base = T; 35 | 36 | fn join(&mut self, other: Self) { 37 | self.0 |= other.0; 38 | } 39 | 40 | fn eval(&self) -> Self::Out { 41 | self.0.clone() 42 | } 43 | 44 | type Add = Unsupported; 45 | type Mul = Unsupported; 46 | type Bounds = Unsupported; 47 | 48 | type Bit = T; 49 | 50 | fn or(&mut self, other: Self::Bit) { 51 | self.0 |= other 52 | } 53 | 54 | fn from_base(base: Self::Base) -> Self { 55 | Self(base) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/types/float.rs: -------------------------------------------------------------------------------- 1 | use crate::num_traits::Number; 2 | use crate::Float; 3 | use crate::{operations::Unsupported, StatValue}; 4 | use bevy_reflect::TypePath; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | /// A stat represented by a floating point number or a fraction. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 9 | #[repr(C, align(8))] 10 | pub struct StatFloat { 11 | addend: T, 12 | min: T, 13 | max: T, 14 | mult: T, 15 | } 16 | 17 | impl Default for StatFloat { 18 | fn default() -> Self { 19 | Self { 20 | addend: T::ZERO, 21 | min: T::MIN_VALUE, 22 | max: T::MAX_VALUE, 23 | mult: T::ONE, 24 | } 25 | } 26 | } 27 | 28 | impl StatValue for StatFloat { 29 | type Out = T; 30 | type Base = T; 31 | 32 | fn join(&mut self, other: Self) { 33 | self.addend += other.addend; 34 | self.mult *= other.mult; 35 | self.min = self.min._max(other.min); 36 | self.max = self.max._min(other.max); 37 | } 38 | 39 | fn eval(&self) -> Self::Out { 40 | (self.addend * self.mult)._min(self.max)._max(self.min) 41 | } 42 | 43 | type Add = T; 44 | type Mul = T; 45 | type Bounds = T; 46 | 47 | type Bit = Unsupported; 48 | 49 | fn add(&mut self, other: Self::Add) { 50 | self.addend += other; 51 | } 52 | 53 | fn mul(&mut self, other: Self::Mul) { 54 | self.mult *= other; 55 | } 56 | 57 | fn min(&mut self, other: Self::Bounds) { 58 | self.min = self.min._max(other) 59 | } 60 | 61 | fn max(&mut self, other: Self::Bounds) { 62 | self.max = self.max._min(other) 63 | } 64 | 65 | fn from_base(base: Self::Base) -> Self { 66 | Self { 67 | addend: base, 68 | min: T::MIN_VALUE, 69 | max: T::MAX_VALUE, 70 | mult: T::ONE, 71 | } 72 | } 73 | } 74 | 75 | /// A stat represented by a floating point number or a fraction, multiplier is additive. 76 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 77 | #[repr(C, align(8))] 78 | pub struct StatFloatAdditive { 79 | addend: T, 80 | min: T, 81 | max: T, 82 | mult: T, 83 | } 84 | 85 | impl Default for StatFloatAdditive { 86 | fn default() -> Self { 87 | Self { 88 | addend: T::ZERO, 89 | min: T::MIN_VALUE, 90 | max: T::MAX_VALUE, 91 | mult: T::ZERO, 92 | } 93 | } 94 | } 95 | 96 | impl StatValue for StatFloatAdditive { 97 | type Out = T; 98 | type Base = T; 99 | 100 | fn join(&mut self, other: Self) { 101 | self.addend += other.addend; 102 | self.mult += other.mult; 103 | self.min = self.min._max(other.min); 104 | self.max = self.max._min(other.max); 105 | } 106 | 107 | fn eval(&self) -> Self::Out { 108 | (self.addend * (self.mult + T::ONE)) 109 | ._min(self.max) 110 | ._max(self.min) 111 | } 112 | 113 | type Add = T; 114 | type Mul = T; 115 | type Bounds = T; 116 | 117 | type Bit = Unsupported; 118 | 119 | fn add(&mut self, other: Self::Add) { 120 | self.addend += other; 121 | } 122 | 123 | fn mul(&mut self, other: Self::Mul) { 124 | self.mult += other; 125 | } 126 | 127 | fn min(&mut self, other: Self::Bounds) { 128 | self.min = self.min._max(other) 129 | } 130 | 131 | fn max(&mut self, other: Self::Bounds) { 132 | self.max = self.max._min(other) 133 | } 134 | 135 | fn from_base(base: Self::Base) -> Self { 136 | Self { 137 | addend: base, 138 | min: T::MIN_VALUE, 139 | max: T::MAX_VALUE, 140 | mult: T::ZERO, 141 | } 142 | } 143 | } 144 | 145 | /// An floating point or fraction based multiplier aggregation. Does not support addition. 146 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 147 | #[repr(C, align(8))] 148 | pub struct StatAdditive { 149 | addend: T, 150 | min: T, 151 | max: T, 152 | } 153 | 154 | impl Default for StatAdditive { 155 | fn default() -> Self { 156 | Self { 157 | addend: T::ZERO, 158 | min: T::MIN_VALUE, 159 | max: T::MAX_VALUE, 160 | } 161 | } 162 | } 163 | 164 | impl StatValue for StatAdditive { 165 | type Out = T; 166 | type Base = T; 167 | 168 | fn join(&mut self, other: Self) { 169 | self.addend += other.addend; 170 | self.min = self.min._max(other.min); 171 | self.max = self.max._min(other.max); 172 | } 173 | 174 | fn eval(&self) -> Self::Out { 175 | self.addend._min(self.max)._max(self.min) 176 | } 177 | 178 | type Add = T; 179 | 180 | type Bit = Unsupported; 181 | 182 | type Mul = Unsupported; 183 | 184 | type Bounds = T; 185 | 186 | fn add(&mut self, other: Self::Add) { 187 | self.addend += other; 188 | } 189 | 190 | fn min(&mut self, other: Self::Bounds) { 191 | self.min = self.min._max(other); 192 | } 193 | 194 | fn max(&mut self, other: Self::Bounds) { 195 | self.max = self.max._min(other); 196 | } 197 | 198 | fn from_base(base: Self::Base) -> Self { 199 | Self { 200 | addend: base, 201 | min: T::MIN_VALUE, 202 | max: T::MAX_VALUE, 203 | } 204 | } 205 | } 206 | 207 | /// An floating point or fraction based multiplier aggregation. Does not support addition. 208 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, TypePath)] 209 | #[repr(C, align(8))] 210 | pub struct StatMult { 211 | min: T, 212 | max: T, 213 | mult: T, 214 | } 215 | 216 | impl Default for StatMult { 217 | fn default() -> Self { 218 | Self { 219 | min: T::MIN_VALUE, 220 | max: T::MAX_VALUE, 221 | mult: T::ONE, 222 | } 223 | } 224 | } 225 | 226 | impl StatValue for StatMult { 227 | type Out = T; 228 | type Base = T; 229 | 230 | fn join(&mut self, other: Self) { 231 | self.mult *= other.mult; 232 | self.min = self.min._max(other.min); 233 | self.max = self.max._min(other.max); 234 | } 235 | 236 | fn eval(&self) -> Self::Out { 237 | self.mult._min(self.max)._max(self.min) 238 | } 239 | 240 | type Add = Unsupported; 241 | 242 | type Bit = Unsupported; 243 | 244 | type Mul = T; 245 | 246 | type Bounds = T; 247 | 248 | fn mul(&mut self, other: Self::Mul) { 249 | self.mult *= other; 250 | } 251 | 252 | fn min(&mut self, other: Self::Bounds) { 253 | self.min = self.min._max(other); 254 | } 255 | 256 | fn max(&mut self, other: Self::Bounds) { 257 | self.max = self.max._min(other); 258 | } 259 | 260 | fn from_base(base: Self::Base) -> Self { 261 | Self { 262 | min: T::MIN_VALUE, 263 | max: T::MAX_VALUE, 264 | mult: base, 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/types/int_pct.rs: -------------------------------------------------------------------------------- 1 | use crate::num_traits::Number; 2 | use crate::Fraction; 3 | use crate::{operations::Unsupported, StatValue}; 4 | use crate::{ 5 | rounding::{Rounding, Truncate}, 6 | Int, 7 | }; 8 | use bevy_reflect::TypePath; 9 | use serde::{Deserialize, Serialize}; 10 | use std::marker::PhantomData; 11 | 12 | /// An integer stat that sums up multipliers additively, 13 | /// then divided by `SCALE`. 14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 15 | #[repr(C, align(8))] 16 | pub struct StatIntPercentAdditive { 17 | addend: T, 18 | mult: T, 19 | min: T, 20 | max: T, 21 | rounding: PhantomData, 22 | } 23 | 24 | impl Default for StatIntPercentAdditive { 25 | fn default() -> Self { 26 | Self { 27 | addend: T::ZERO, 28 | min: T::MIN_VALUE, 29 | max: T::MAX_VALUE, 30 | mult: T::ZERO, 31 | rounding: PhantomData, 32 | } 33 | } 34 | } 35 | 36 | impl StatValue for StatIntPercentAdditive { 37 | type Out = T; 38 | type Base = T; 39 | 40 | fn join(&mut self, other: Self) { 41 | self.addend += other.addend; 42 | self.mult += other.mult; 43 | self.max = self.max.min(other.max); 44 | self.min = self.min.max(other.min); 45 | } 46 | 47 | fn eval(&self) -> Self::Out { 48 | let numer = self.addend * (self.mult + T::from_i64(S)); 49 | let base = T::from_fraction(R::round(numer.build_fraction(T::from_i64(S)))); 50 | base.min(self.max).max(self.min) 51 | } 52 | 53 | type Add = T; 54 | type Mul = T; 55 | type Bounds = T; 56 | 57 | type Bit = Unsupported; 58 | 59 | fn add(&mut self, other: Self::Add) { 60 | self.addend += other; 61 | } 62 | 63 | fn mul(&mut self, other: Self::Mul) { 64 | // Since this is "sum the multipliers" 65 | self.mult += other; 66 | } 67 | 68 | fn min(&mut self, other: Self::Bounds) { 69 | self.min = self.min.max(other) 70 | } 71 | 72 | fn max(&mut self, other: Self::Bounds) { 73 | self.max = self.max.min(other) 74 | } 75 | 76 | fn from_base(base: Self::Base) -> Self { 77 | Self { 78 | addend: base, 79 | min: T::MIN_VALUE, 80 | max: T::MAX_VALUE, 81 | mult: T::ZERO, 82 | rounding: PhantomData, 83 | } 84 | } 85 | } 86 | 87 | /// An integer stat with integer multipliers divided by `SCALE`. 88 | /// 89 | /// Calculated as a fraction. 90 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 91 | #[serde(bound(serialize = "T: Int + Serialize, R: Rounding"))] 92 | #[serde(bound(deserialize = "T: Int> + Deserialize<'de>, R: Rounding"))] 93 | #[repr(C, align(8))] 94 | pub struct StatIntPercent { 95 | addend: T, 96 | mult: Fraction, 97 | min: T, 98 | max: T, 99 | rounding: PhantomData, 100 | } 101 | 102 | impl Default for StatIntPercent { 103 | fn default() -> Self { 104 | Self { 105 | addend: T::ONE, 106 | min: T::MIN_VALUE, 107 | max: T::MAX_VALUE, 108 | mult: Fraction::ONE, 109 | rounding: PhantomData, 110 | } 111 | } 112 | } 113 | 114 | impl StatValue for StatIntPercent { 115 | type Out = T; 116 | type Base = T; 117 | 118 | fn join(&mut self, other: Self) { 119 | self.addend += other.addend; 120 | self.mult += other.mult; 121 | self.max = self.max.min(other.max); 122 | self.min = self.min.max(other.min); 123 | } 124 | 125 | fn eval(&self) -> Self::Out { 126 | let fraction = self.addend.into_fraction() * self.mult; 127 | let int = T::from_fraction(R::round(fraction)); 128 | int.min(self.max).max(self.min) 129 | } 130 | 131 | type Add = T; 132 | type Mul = T; 133 | type Bounds = T; 134 | 135 | type Bit = Unsupported; 136 | 137 | fn add(&mut self, other: Self::Add) { 138 | self.addend += other; 139 | } 140 | 141 | fn mul(&mut self, other: Self::Mul) { 142 | self.mult *= T::build_fraction(other, T::from_i64(S)); 143 | } 144 | 145 | fn min(&mut self, other: Self::Bounds) { 146 | self.min = self.min.max(other) 147 | } 148 | 149 | fn max(&mut self, other: Self::Bounds) { 150 | self.max = self.max.min(other) 151 | } 152 | 153 | fn from_base(base: Self::Base) -> Self { 154 | Self { 155 | addend: base, 156 | min: T::MIN_VALUE, 157 | max: T::MAX_VALUE, 158 | mult: Fraction::ONE, 159 | rounding: PhantomData, 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/types/int_ratio.rs: -------------------------------------------------------------------------------- 1 | use crate::{operations::Unsupported, StatValue}; 2 | use crate::{ 3 | rounding::{Rounding, Truncate}, 4 | Float, Int, 5 | }; 6 | use crate::{Fraction, NumCast}; 7 | use bevy_reflect::TypePath; 8 | use serde::{Deserialize, Serialize}; 9 | use std::marker::PhantomData; 10 | 11 | /// A stat represented by an integer, does not support floating point multipliers. 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TypePath)] 13 | #[repr(C, align(8))] 14 | pub struct StatInt { 15 | addend: T, 16 | min: T, 17 | max: T, 18 | mult: T, 19 | } 20 | 21 | impl Default for StatInt { 22 | fn default() -> Self { 23 | Self { 24 | addend: T::ZERO, 25 | min: T::MIN_VALUE, 26 | max: T::MAX_VALUE, 27 | mult: T::ONE, 28 | } 29 | } 30 | } 31 | 32 | impl StatValue for StatInt { 33 | type Out = T; 34 | type Base = T; 35 | 36 | fn join(&mut self, other: Self) { 37 | self.addend += other.addend; 38 | self.mult *= other.mult; 39 | self.min = self.min.max(other.min); 40 | self.max = self.max.min(other.max); 41 | } 42 | 43 | fn eval(&self) -> Self::Out { 44 | (self.addend * self.mult).min(self.max).max(self.min) 45 | } 46 | 47 | type Add = T; 48 | type Mul = T; 49 | type Bounds = T; 50 | 51 | type Bit = Unsupported; 52 | 53 | fn add(&mut self, other: Self::Add) { 54 | self.addend += other; 55 | } 56 | 57 | fn mul(&mut self, other: Self::Mul) { 58 | self.mult *= other; 59 | } 60 | 61 | fn min(&mut self, other: Self::Bounds) { 62 | self.min = self.min.max(other) 63 | } 64 | 65 | fn max(&mut self, other: Self::Bounds) { 66 | self.max = self.max.min(other) 67 | } 68 | 69 | fn from_base(base: Self::Base) -> Self { 70 | Self { 71 | addend: base, 72 | min: T::MIN_VALUE, 73 | max: T::MAX_VALUE, 74 | mult: T::ONE, 75 | } 76 | } 77 | } 78 | 79 | /// An integer stat that multiplies with floating point numbers and rounds back to an integer. 80 | #[derive(Debug, Clone, Copy, PartialEq, Eq, TypePath, Serialize, Deserialize)] 81 | #[repr(C, align(8))] 82 | pub struct StatIntRounded { 83 | addend: T, 84 | min: T, 85 | max: T, 86 | mult: F, 87 | rounding: PhantomData, 88 | } 89 | 90 | impl StatIntRounded, R> { 91 | pub fn reduce(&mut self) { 92 | self.mult = self.mult.reduced(); 93 | } 94 | 95 | pub fn reduced(mut self) -> Self { 96 | self.mult = self.mult.reduced(); 97 | self 98 | } 99 | } 100 | 101 | impl Default for StatIntRounded { 102 | fn default() -> Self { 103 | Self { 104 | addend: T::ZERO, 105 | min: T::MIN_VALUE, 106 | max: T::MAX_VALUE, 107 | mult: F::ONE, 108 | rounding: Default::default(), 109 | } 110 | } 111 | } 112 | 113 | impl StatValue for StatIntRounded 114 | where 115 | T: NumCast, 116 | F: NumCast, 117 | { 118 | type Out = T; 119 | type Base = T; 120 | 121 | fn join(&mut self, other: Self) { 122 | self.addend += other.addend; 123 | self.mult *= other.mult; 124 | self.min = self.min.max(other.min); 125 | self.max = self.max.min(other.max); 126 | } 127 | 128 | fn eval(&self) -> Self::Out { 129 | let val = self.addend.cast() * self.mult; 130 | let int_val: T = R::round(val).cast(); 131 | int_val.min(self.max).max(self.min) 132 | } 133 | 134 | type Add = T; 135 | type Mul = F; 136 | type Bounds = T; 137 | 138 | type Bit = Unsupported; 139 | 140 | fn add(&mut self, other: Self::Add) { 141 | self.addend += other; 142 | } 143 | 144 | fn mul(&mut self, other: Self::Mul) { 145 | self.mult *= other; 146 | } 147 | 148 | fn min(&mut self, other: Self::Bounds) { 149 | self.min = self.min.max(other); 150 | } 151 | 152 | fn max(&mut self, other: Self::Bounds) { 153 | self.max = self.max.min(other); 154 | } 155 | 156 | fn from_base(base: Self::Base) -> Self { 157 | Self { 158 | addend: base, 159 | min: T::MIN_VALUE, 160 | max: T::MAX_VALUE, 161 | mult: F::ONE, 162 | rounding: Default::default(), 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod flags; 2 | mod float; 3 | mod int_pct; 4 | mod int_ratio; 5 | mod prioritized; 6 | pub use flags::StatFlags; 7 | pub use float::{StatAdditive, StatFloat, StatFloatAdditive, StatMult}; 8 | pub use int_pct::{StatIntPercent, StatIntPercentAdditive}; 9 | pub use int_ratio::{StatInt, StatIntRounded}; 10 | pub use prioritized::Prioritized; 11 | -------------------------------------------------------------------------------- /src/types/prioritized.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use bevy_reflect::TypePath; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{operations::Unsupported, Shareable, StatValue}; 7 | 8 | /// A prioritized attribute that evaluates to the first or 9 | /// last occurrence with the highest priority. 10 | /// 11 | /// The [`Default`] priority is `i32::MIN`, if created via `From` or `from_base`, 12 | /// priority is 0. 13 | #[derive(Debug, Clone, Copy, TypePath, Serialize, Deserialize)] 14 | #[repr(C)] 15 | pub struct Prioritized { 16 | value: T, 17 | priority: i32, 18 | } 19 | 20 | impl Default for Prioritized { 21 | fn default() -> Self { 22 | Self { 23 | value: Default::default(), 24 | priority: i32::MIN, 25 | } 26 | } 27 | } 28 | 29 | impl Prioritized { 30 | pub const fn new(value: T, priority: i32) -> Self { 31 | Prioritized { value, priority } 32 | } 33 | 34 | pub const fn new_min(value: T) -> Self { 35 | Prioritized { 36 | value, 37 | priority: i32::MIN, 38 | } 39 | } 40 | 41 | pub const fn get(&self) -> &T { 42 | &self.value 43 | } 44 | 45 | pub fn into_inner(self) -> T { 46 | self.value 47 | } 48 | } 49 | 50 | impl From for Prioritized { 51 | fn from(value: T) -> Self { 52 | Prioritized { value, priority: 0 } 53 | } 54 | } 55 | 56 | impl StatValue for Prioritized { 57 | type Out = T; 58 | 59 | #[allow(clippy::collapsible_else_if)] 60 | fn join(&mut self, other: Self) { 61 | if R { 62 | if self.priority <= other.priority { 63 | self.value = other.value 64 | } 65 | } else { 66 | if self.priority < other.priority { 67 | self.value = other.value 68 | } 69 | } 70 | } 71 | 72 | fn eval(&self) -> Self::Out { 73 | self.value.clone() 74 | } 75 | 76 | type Add = Unsupported; 77 | 78 | type Mul = Unsupported; 79 | 80 | type Bit = Self; 81 | 82 | type Bounds = Unsupported; 83 | 84 | type Base = T; 85 | 86 | fn from_base(base: Self::Base) -> Self { 87 | Self { 88 | value: base, 89 | priority: 0, 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/asset.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::{Asset, AssetApp, AssetPlugin, Assets, Handle}, 3 | prelude::{ResMut, Single}, 4 | }; 5 | use bevy_app::App; 6 | use bevy_ecs::{ 7 | component::Component, 8 | entity::Entity, 9 | query::{QueryData, With}, 10 | system::{Commands, Res, RunSystemOnce}, 11 | }; 12 | use bevy_reflect::TypePath; 13 | use bevy_stat_query::{ 14 | types::StatFloat, ChildQuery, QualifierQuery, Querier, QueryStream, Stat, StatEntities, 15 | StatEntity, StatValue, StatValuePair, 16 | }; 17 | 18 | #[derive(Debug, Clone, Copy, Stat)] 19 | #[stat(value = "StatFloat")] 20 | pub enum Stats { 21 | Damage, 22 | Defense, 23 | } 24 | 25 | #[derive(Asset, TypePath)] 26 | pub struct Weapon { 27 | pub damage: f32, 28 | } 29 | 30 | #[derive(Debug, Component)] 31 | pub struct WeaponHandle(Handle); 32 | 33 | #[derive(Component)] 34 | pub struct WeaponState { 35 | pub durability: f32, 36 | } 37 | 38 | #[derive(Component)] 39 | pub struct A; 40 | 41 | #[derive(Component)] 42 | pub struct B; 43 | 44 | #[derive(QueryData)] 45 | pub struct WeaponQuery { 46 | weapon: &'static WeaponHandle, 47 | state: &'static WeaponState, 48 | } 49 | 50 | impl QueryStream for WeaponQuery { 51 | type Qualifier = u32; 52 | type Context = Res<'static, Assets>; 53 | type Query = WeaponQuery; 54 | 55 | fn stream_stat( 56 | query: WeaponQueryItem, 57 | context: &Res>, 58 | _: Entity, 59 | _: &QualifierQuery, 60 | stat_value: &mut StatValuePair, 61 | _: Querier, 62 | ) { 63 | if let Some(value) = stat_value.is_then_cast(&Stats::Damage) { 64 | let Some(weapon) = context.get(query.weapon.0.id()) else { 65 | return; 66 | }; 67 | value.add(weapon.damage * query.state.durability) 68 | } 69 | } 70 | } 71 | 72 | #[test] 73 | pub fn asset_test() { 74 | let mut app = App::new(); 75 | app.add_plugins(AssetPlugin::default()) 76 | .init_asset::(); 77 | app.world_mut().run_system_once(init).unwrap(); 78 | app.world_mut().flush(); 79 | app.world_mut().run_system_once(query).unwrap(); 80 | } 81 | 82 | fn init(mut commands: Commands, mut assets: ResMut>) { 83 | commands.spawn((StatEntity, A)).with_children(|x| { 84 | x.spawn(( 85 | WeaponHandle(assets.add(Weapon { damage: 4.0 })), 86 | WeaponState { durability: 0.5 }, 87 | )); 88 | }); 89 | commands.spawn((StatEntity, B)).with_children(|x| { 90 | x.spawn(( 91 | WeaponHandle(assets.add(Weapon { damage: 8.0 })), 92 | WeaponState { durability: 1.0 }, 93 | )); 94 | x.spawn(( 95 | WeaponHandle(assets.add(Weapon { damage: 6.0 })), 96 | WeaponState { durability: 2.0 }, 97 | )); 98 | }); 99 | } 100 | 101 | fn query( 102 | querier: StatEntities, 103 | weapon_query: ChildQuery, 104 | a: Single, With)>, 105 | b: Single, With)>, 106 | ) { 107 | let querier = querier.join(&weapon_query); 108 | assert_eq!( 109 | querier.eval_stat(*a, &QualifierQuery::Aggregate(0u32), &Stats::Damage), 110 | Some(2.0) 111 | ); 112 | assert_eq!( 113 | querier.eval_stat(*a, &QualifierQuery::Aggregate(0u32), &Stats::Defense), 114 | Some(0.0) 115 | ); 116 | 117 | assert_eq!( 118 | querier.eval_stat(*b, &QualifierQuery::Aggregate(0u32), &Stats::Damage), 119 | Some(20.0) 120 | ); 121 | assert_eq!( 122 | querier.eval_stat(*b, &QualifierQuery::Aggregate(0u32), &Stats::Defense), 123 | Some(0.0) 124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /tests/complex.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | asset::AssetPlugin, 3 | prelude::{Single, With}, 4 | }; 5 | use bevy_app::App; 6 | use bevy_ecs::{ 7 | component::Component, 8 | entity::Entity, 9 | system::{Commands, RunSystemOnce}, 10 | }; 11 | use bevy_stat_query::{ 12 | match_stat, types::StatFloat, ChildQuery, Qualifier, QualifierFlag, QualifierQuery, Querier, 13 | Stat, StatEntities, StatEntity, StatExtension, StatMap, StatQuery, StatStream, StatValue, 14 | StatValuePair, 15 | }; 16 | 17 | bitflags::bitflags! { 18 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 19 | pub struct Adjective: u32 { 20 | const Fire = 1; 21 | const Water = 2; 22 | const Earth = 4; 23 | const Air = 8; 24 | } 25 | } 26 | 27 | #[derive(Debug, Clone, Copy, Stat)] 28 | #[stat(value = "StatFloat")] 29 | pub enum Stats { 30 | WeaponDamage, 31 | Damage, 32 | Defense, 33 | Strength, 34 | WeaponProficiency, 35 | } 36 | 37 | #[derive(Component)] 38 | pub struct Main; 39 | 40 | #[derive(Component)] 41 | pub struct Weapon { 42 | pub damage: f32, 43 | } 44 | 45 | impl StatStream for Weapon { 46 | type Qualifier = Adjective; 47 | 48 | fn stream_stat( 49 | &self, 50 | _: Entity, 51 | _: &QualifierQuery, 52 | stat_value: &mut StatValuePair, 53 | _: Querier, 54 | ) { 55 | if let Some(value) = stat_value.is_then_cast(&Stats::WeaponDamage) { 56 | value.add(self.damage); 57 | } 58 | } 59 | } 60 | 61 | #[derive(Component)] 62 | pub struct StrengthBuff { 63 | qualifier: Qualifier, 64 | multiplier: f32, 65 | } 66 | 67 | impl StatStream for StrengthBuff { 68 | type Qualifier = Adjective; 69 | 70 | fn stream_stat( 71 | &self, 72 | _: Entity, 73 | qualifier: &QualifierQuery, 74 | stat_value: &mut StatValuePair, 75 | _: Querier, 76 | ) { 77 | if let Some(value) = stat_value.is_then_cast(&Stats::Strength) { 78 | if qualifier.qualify(&self.qualifier) { 79 | value.mul(self.multiplier); 80 | } 81 | } 82 | } 83 | } 84 | 85 | #[derive(Component)] 86 | pub struct DamageBuff { 87 | qualifier: Qualifier, 88 | multiplier: f32, 89 | } 90 | 91 | impl StatStream for DamageBuff { 92 | type Qualifier = Adjective; 93 | 94 | fn stream_stat( 95 | &self, 96 | _: Entity, 97 | qualifier: &QualifierQuery, 98 | stat_value: &mut StatValuePair, 99 | _: Querier, 100 | ) { 101 | if let Some(value) = stat_value.is_then_cast(&Stats::Damage) { 102 | if qualifier.qualify(&self.qualifier) { 103 | value.mul(self.multiplier); 104 | } 105 | } 106 | } 107 | } 108 | 109 | #[test] 110 | pub fn test() { 111 | let mut app = App::new(); 112 | app.add_plugins(AssetPlugin::default()) 113 | .register_stat_relation::(|entity, qualifier, stat, querier| { 114 | match_stat!(stat => { 115 | (Stats::Damage, value) => { 116 | value.add( 117 | querier.eval_stat(entity, qualifier, &Stats::Strength).unwrap() 118 | ); 119 | value.add( 120 | querier.eval_stat(entity, qualifier, &Stats::WeaponDamage).unwrap() * 121 | querier.eval_stat(entity, qualifier, &Stats::WeaponProficiency).unwrap() 122 | ); 123 | } 124 | }) 125 | }); 126 | app.world_mut().run_system_once(init).unwrap(); 127 | app.world_mut().flush(); 128 | app.world_mut().run_system_once(query).unwrap(); 129 | } 130 | 131 | fn init(mut commands: Commands) { 132 | commands 133 | .spawn(( 134 | StatEntity, 135 | { 136 | let mut map = StatMap::::new(); 137 | map.insert_base(Default::default(), Stats::Strength, 4.0); 138 | map.insert_base(Default::default(), Stats::WeaponProficiency, 0.5); 139 | map 140 | }, 141 | Weapon { damage: 12.0 }, 142 | Main, 143 | )) 144 | .with_children(|c| { 145 | c.spawn(StrengthBuff { 146 | qualifier: Adjective::none().into(), 147 | multiplier: 1.5, 148 | }); 149 | c.spawn(StrengthBuff { 150 | qualifier: Adjective::Fire.into(), 151 | multiplier: 2.0, 152 | }); 153 | c.spawn(StrengthBuff { 154 | qualifier: Adjective::Water.into(), 155 | multiplier: 0.5, 156 | }); 157 | // elemental damage 158 | c.spawn(DamageBuff { 159 | qualifier: Qualifier { 160 | all_of: Adjective::none(), 161 | any_of: Adjective::all(), 162 | }, 163 | multiplier: 2.0, 164 | }); 165 | c.spawn(DamageBuff { 166 | qualifier: Adjective::Fire.into(), 167 | multiplier: 1.5, 168 | }); 169 | }); 170 | } 171 | 172 | fn query( 173 | entities: Single>, 174 | querier: StatEntities, 175 | base_stat_query: StatQuery>, 176 | weapon_query: StatQuery, 177 | strength_buffs: ChildQuery, 178 | damage_buffs: ChildQuery, 179 | ) { 180 | let entity = *entities; 181 | let querier = querier 182 | .join(&base_stat_query) 183 | .join(&weapon_query) 184 | .join(&strength_buffs) 185 | .join(&damage_buffs); 186 | 187 | // 4 * 1.5 + 12 * 0.5 188 | assert_eq!( 189 | querier.eval_stat(entity, &Default::default(), &Stats::Damage), 190 | Some(12.0) 191 | ); 192 | // (4 * 1.5 * 2 + 12 * 0.5) * 2 * 1.5 193 | assert_eq!( 194 | querier.eval_stat(entity, &Adjective::Fire.into(), &Stats::Damage), 195 | Some(54.0) 196 | ); 197 | // (4 * 1.5 * 0.5 + 12 * 0.5) * 2 198 | assert_eq!( 199 | querier.eval_stat(entity, &Adjective::Water.into(), &Stats::Damage), 200 | Some(18.0) 201 | ); 202 | // (4 * 1.5 * 2 * 0.5 + 12 * 0.5) * 2 * 1.5 203 | assert_eq!( 204 | querier.eval_stat( 205 | entity, 206 | &(Adjective::Fire | Adjective::Water).into(), 207 | &Stats::Damage 208 | ), 209 | Some(36.0) 210 | ); 211 | } 212 | -------------------------------------------------------------------------------- /tests/derive.rs: -------------------------------------------------------------------------------- 1 | use bevy_stat_query::types::StatIntRounded; 2 | use bevy_stat_query::Attribute; 3 | use bevy_stat_query::Stat; 4 | 5 | #[derive(Debug, Clone, Copy, Stat, PartialEq, Eq)] 6 | #[stat(value = "StatIntRounded")] 7 | pub enum Stats { 8 | A, 9 | B, 10 | C, 11 | D, 12 | } 13 | 14 | #[derive(Debug, Clone, Copy, Stat, PartialEq, Eq)] 15 | #[stat(value = "StatIntRounded")] 16 | pub enum NumStats { 17 | E = 2, 18 | F = 0, 19 | G, 20 | H = 3, 21 | } 22 | 23 | #[derive(Debug, Clone, Copy, Stat, PartialEq, Eq)] 24 | #[stat(value = "StatIntRounded")] 25 | pub struct X; 26 | 27 | use NumStats::*; 28 | use Stats::*; 29 | 30 | #[derive(Debug, Attribute)] 31 | pub struct IsDragon; 32 | 33 | #[derive(Debug, Attribute)] 34 | #[repr(u64)] 35 | pub enum CreatureType { 36 | Beast, 37 | Dragon, 38 | } 39 | #[derive(Attribute)] 40 | pub struct CreatureAbility(u64); 41 | 42 | bitflags::bitflags! { 43 | impl CreatureAbility: u64 { 44 | const Fire = 1; 45 | const Ice = 2; 46 | } 47 | } 48 | 49 | #[test] 50 | pub fn test_derive() { 51 | assert_eq!(Stats::from_index(Stat::as_index(&A)), A); 52 | assert_eq!(Stats::from_index(Stat::as_index(&B)), B); 53 | assert_eq!(Stats::from_index(Stat::as_index(&C)), C); 54 | assert_eq!(Stats::from_index(Stat::as_index(&D)), D); 55 | assert_eq!(NumStats::from_index(Stat::as_index(&E)), E); 56 | assert_eq!(NumStats::from_index(Stat::as_index(&F)), F); 57 | assert_eq!(NumStats::from_index(Stat::as_index(&G)), G); 58 | assert_eq!(NumStats::from_index(Stat::as_index(&H)), H); 59 | assert_eq!(Stats::values().into_iter().count(), 4); 60 | assert_eq!(NumStats::values().into_iter().count(), 4); 61 | assert_eq!(A.name(), "A"); 62 | assert_eq!(B.name(), "B"); 63 | assert_eq!(C.name(), "C"); 64 | assert_eq!(D.name(), "D"); 65 | assert_eq!(E.name(), "E"); 66 | assert_eq!(F.name(), "F"); 67 | assert_eq!(G.name(), "G"); 68 | assert_eq!(H.name(), "H"); 69 | assert_eq!(X::from_index(Stat::as_index(&X)), X); 70 | assert_eq!(X::values().into_iter().count(), 1); 71 | assert_eq!(X.name(), "X"); 72 | } 73 | -------------------------------------------------------------------------------- /tests/distance.rs: -------------------------------------------------------------------------------- 1 | use bevy_ecs::{component::Component, entity::Entity, system::RunSystemOnce, world::World}; 2 | use bevy_reflect::TypePath; 3 | use bevy_stat_query::{ 4 | types::{Prioritized, StatInt}, 5 | ChildQuery, QualifierQuery, Querier, Stat, StatEntities, StatEntity, StatExtension, 6 | StatQueryMut, StatStream, StatVTable, StatValue, StatValuePair, 7 | }; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, Clone, Copy, Stat)] 11 | #[stat(value = "Prioritized")] 12 | pub struct StatDistance; 13 | 14 | #[derive(Debug, Clone, Copy, Stat)] 15 | #[stat(value = "Prioritized")] 16 | pub struct StatAllegiance; 17 | 18 | #[derive( 19 | Debug, 20 | Clone, 21 | Copy, 22 | PartialEq, 23 | Eq, 24 | PartialOrd, 25 | Ord, 26 | Hash, 27 | Default, 28 | Component, 29 | TypePath, 30 | Serialize, 31 | Deserialize, 32 | )] 33 | pub enum Allegiance { 34 | #[default] 35 | Player, 36 | AI, 37 | } 38 | 39 | #[derive( 40 | Debug, 41 | Clone, 42 | Copy, 43 | PartialEq, 44 | Eq, 45 | PartialOrd, 46 | Ord, 47 | Hash, 48 | Default, 49 | Component, 50 | TypePath, 51 | Serialize, 52 | Deserialize, 53 | )] 54 | pub enum Relation { 55 | #[default] 56 | Ally, 57 | Enemy, 58 | } 59 | 60 | #[derive(Component)] 61 | pub struct Position([i32; 2]); 62 | 63 | #[derive(Component)] 64 | pub struct A; 65 | 66 | #[derive(Component)] 67 | pub struct B; 68 | 69 | impl StatStream for Position { 70 | type Qualifier = bool; 71 | 72 | fn stream_relation( 73 | &self, 74 | other: &Self, 75 | _: Entity, 76 | _: Entity, 77 | _: &QualifierQuery, 78 | stat_value: &mut StatValuePair, 79 | _: Querier, 80 | ) { 81 | if let Some(v) = stat_value.is_then_cast(&StatDistance) { 82 | v.join(Prioritized::from( 83 | (self.0[0] - other.0[0]).abs() + (self.0[1] - other.0[1]).abs(), 84 | )) 85 | } 86 | } 87 | } 88 | 89 | impl StatStream for Allegiance { 90 | type Qualifier = bool; 91 | 92 | fn stream_relation( 93 | &self, 94 | other: &Self, 95 | _: Entity, 96 | _: Entity, 97 | _: &QualifierQuery, 98 | stat_value: &mut StatValuePair, 99 | _: Querier, 100 | ) { 101 | if let Some(v) = stat_value.is_then_cast(&StatAllegiance) { 102 | if self == other { 103 | v.join(Relation::Ally.into()) 104 | } else { 105 | v.join(Relation::Enemy.into()) 106 | } 107 | } 108 | } 109 | } 110 | 111 | #[derive(Component)] 112 | pub struct DistanceAura(Entity); 113 | 114 | #[derive(Component)] 115 | pub struct AllegianceAura(i32, Entity); 116 | 117 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 118 | pub enum StatEffects { 119 | Distance, 120 | Allegiance, 121 | } 122 | 123 | impl Stat for StatEffects { 124 | type Value = StatInt; 125 | 126 | fn name(&self) -> &'static str { 127 | match self { 128 | StatEffects::Distance => "DistanceEffect", 129 | StatEffects::Allegiance => "AllegianceEffect", 130 | } 131 | } 132 | 133 | fn values() -> impl IntoIterator { 134 | [Self::Distance, Self::Allegiance] 135 | } 136 | 137 | fn vtable() -> &'static bevy_stat_query::StatVTable { 138 | static VTABLE: StatVTable = StatVTable::of::(); 139 | &VTABLE 140 | } 141 | 142 | fn as_index(&self) -> u64 { 143 | match self { 144 | StatEffects::Distance => 0, 145 | StatEffects::Allegiance => 1, 146 | } 147 | } 148 | 149 | fn from_index(index: u64) -> Self { 150 | match index { 151 | 0 => StatEffects::Distance, 152 | _ => StatEffects::Allegiance, 153 | } 154 | } 155 | } 156 | 157 | impl StatStream for DistanceAura { 158 | type Qualifier = bool; 159 | 160 | fn stream_stat( 161 | &self, 162 | entity: Entity, 163 | qualifier: &QualifierQuery, 164 | stat_value: &mut StatValuePair, 165 | querier: Querier, 166 | ) { 167 | if let Some(v) = stat_value.is_then_cast(&StatEffects::Distance) { 168 | // could panic or return default or write to ctx etc. 169 | let distance = querier 170 | .query_relation(self.0, entity, qualifier, &StatDistance) 171 | .unwrap() 172 | .into_inner(); 173 | v.add(distance); 174 | } 175 | } 176 | } 177 | 178 | impl StatStream for AllegianceAura { 179 | type Qualifier = bool; 180 | 181 | fn stream_stat( 182 | &self, 183 | entity: Entity, 184 | qualifier: &QualifierQuery, 185 | stat_value: &mut StatValuePair, 186 | querier: Querier, 187 | ) { 188 | if let Some(v) = stat_value.is_then_cast(&StatEffects::Allegiance) { 189 | let distance = querier 190 | .query_relation(self.1, entity, qualifier, &StatAllegiance) 191 | .unwrap() 192 | .into_inner(); 193 | v.add(match distance { 194 | Relation::Ally => self.0, 195 | Relation::Enemy => 0, 196 | }); 197 | } 198 | } 199 | } 200 | 201 | #[test] 202 | pub fn main() { 203 | let mut world = World::new(); 204 | world.register_stat::(); 205 | world.register_stat::(); 206 | let a = world 207 | .spawn((StatEntity, Position([-1, 7]), Allegiance::Player, A)) 208 | .id(); 209 | let b = world 210 | .spawn((StatEntity, Position([4, 5]), Allegiance::AI, B)) 211 | .id(); 212 | world.entity_mut(a).with_children(|f| { 213 | f.spawn((DistanceAura(b), AllegianceAura(5, b))); 214 | }); 215 | world.entity_mut(b).with_children(|f| { 216 | f.spawn((DistanceAura(a), AllegianceAura(7, a))); 217 | }); 218 | let _ = world.run_system_once({ 219 | move |query: StatEntities, 220 | mut allegiance: StatQueryMut, 221 | mut position: StatQueryMut, 222 | allegiance_aura: ChildQuery, 223 | distance_aura: ChildQuery| { 224 | macro_rules! querier { 225 | () => { 226 | query 227 | .join(&allegiance) 228 | .join(&position) 229 | .join(&allegiance_aura) 230 | .join(&distance_aura) 231 | }; 232 | } 233 | assert_eq!( 234 | querier!().eval_stat(a, &QualifierQuery::Aggregate(false), &StatEffects::Distance), 235 | Some(7) 236 | ); 237 | assert_eq!( 238 | querier!().eval_stat(b, &QualifierQuery::Aggregate(false), &StatEffects::Distance), 239 | Some(7) 240 | ); 241 | position.query.get_mut(a).unwrap().0[1] = -7; 242 | assert_eq!( 243 | querier!().eval_stat(a, &QualifierQuery::Aggregate(false), &StatEffects::Distance), 244 | Some(17) 245 | ); 246 | assert_eq!( 247 | querier!().eval_stat(b, &QualifierQuery::Aggregate(false), &StatEffects::Distance), 248 | Some(17) 249 | ); 250 | assert_eq!( 251 | querier!().eval_stat( 252 | a, 253 | &QualifierQuery::Aggregate(false), 254 | &StatEffects::Allegiance 255 | ), 256 | Some(0) 257 | ); 258 | assert_eq!( 259 | querier!().eval_stat( 260 | b, 261 | &QualifierQuery::Aggregate(false), 262 | &StatEffects::Allegiance 263 | ), 264 | Some(0) 265 | ); 266 | *allegiance.query.get_mut(b).unwrap() = Allegiance::Player; 267 | 268 | assert_eq!( 269 | querier!().eval_stat( 270 | a, 271 | &QualifierQuery::Aggregate(false), 272 | &StatEffects::Allegiance 273 | ), 274 | Some(5) 275 | ); 276 | assert_eq!( 277 | querier!().eval_stat( 278 | b, 279 | &QualifierQuery::Aggregate(false), 280 | &StatEffects::Allegiance 281 | ), 282 | Some(7) 283 | ); 284 | } 285 | }); 286 | } 287 | -------------------------------------------------------------------------------- /tests/fraction.rs: -------------------------------------------------------------------------------- 1 | use bevy_stat_query::Fraction; 2 | 3 | #[test] 4 | pub fn reduction() { 5 | macro_rules! test_reduction { 6 | ($a: expr, $b: expr) => {{ 7 | let v = Fraction::new($a, $b); 8 | assert_eq!(v, Fraction::new_raw($a, $b)); 9 | let v = Fraction::::const_new($a, $b); 10 | assert_eq!(v, Fraction::new_raw($a, $b)); 11 | let v = Fraction::new_raw($a, $b); 12 | assert_eq!(v.reduced(), Fraction::new_raw($a, $b)); 13 | }}; 14 | ($a: expr, $b: expr, $c: expr, $d: expr) => {{ 15 | let v = Fraction::new($a, $b); 16 | assert_eq!(v, Fraction::new_raw($c, $d)); 17 | let v = Fraction::::const_new($a, $b); 18 | assert_eq!(v, Fraction::new_raw($c, $d)); 19 | let v = Fraction::new_raw($a, $b); 20 | assert_eq!(v.reduced(), Fraction::new_raw($c, $d)); 21 | }}; 22 | } 23 | test_reduction!(1, 1); 24 | test_reduction!(0, 1); 25 | test_reduction!(1, 0); 26 | test_reduction!(2, 1); 27 | test_reduction!(4, 1); 28 | test_reduction!(1, 3); 29 | test_reduction!(2, 3); 30 | test_reduction!(-2, 3); 31 | test_reduction!(-3, 2); 32 | test_reduction!(-2, -3); 33 | test_reduction!(-5, -3); 34 | test_reduction!(2, 4, 1, 2); 35 | test_reduction!(4, 6, 2, 3); 36 | test_reduction!(15, 10, 3, 2); 37 | test_reduction!(-25, 25, -1, 1); 38 | test_reduction!(-40, -60, -2, -3); 39 | } 40 | 41 | #[test] 42 | pub fn rounding() { 43 | fn f(a: i32, b: i32) -> Fraction { 44 | Fraction::new_raw(a, b) 45 | } 46 | 47 | assert_eq!(f(0, 1).floor(), 0); 48 | assert_eq!(f(0, 1).ceil(), 0); 49 | assert_eq!(f(0, 1).round(), 0); 50 | assert_eq!(f(0, 1).trunc(), 0); 51 | 52 | assert_eq!(f(1, 1).floor(), 1); 53 | assert_eq!(f(1, 1).ceil(), 1); 54 | assert_eq!(f(1, 1).round(), 1); 55 | assert_eq!(f(1, 1).trunc(), 1); 56 | 57 | assert_eq!(f(1, 2).floor(), 0); 58 | assert_eq!(f(1, 2).ceil(), 1); 59 | assert_eq!(f(1, 2).round(), 1); 60 | assert_eq!(f(1, 2).trunc(), 0); 61 | 62 | assert_eq!(f(5, 2).floor(), 2); 63 | assert_eq!(f(5, 2).ceil(), 3); 64 | assert_eq!(f(5, 2).round(), 3); 65 | assert_eq!(f(5, 2).trunc(), 2); 66 | 67 | assert_eq!(f(-1, 1).floor(), -1); 68 | assert_eq!(f(-1, 1).ceil(), -1); 69 | assert_eq!(f(-1, 1).round(), -1); 70 | assert_eq!(f(-1, 1).trunc(), -1); 71 | 72 | assert_eq!(f(1, -2).floor(), -1); 73 | assert_eq!(f(1, -2).ceil(), 0); 74 | assert_eq!(f(1, -2).round(), -1); 75 | assert_eq!(f(1, -2).trunc(), 0); 76 | 77 | assert_eq!(f(7, 3).floor(), 2); 78 | assert_eq!(f(7, 3).ceil(), 3); 79 | assert_eq!(f(7, 3).trunc(), 2); 80 | assert_eq!(f(7, 3).round(), 2); 81 | assert_eq!(f(8, 3).round(), 3); 82 | 83 | assert_eq!(f(7, -3).floor(), -3); 84 | assert_eq!(f(7, -3).ceil(), -2); 85 | assert_eq!(f(7, -3).trunc(), -2); 86 | assert_eq!(f(7, -3).trunc(), -2); 87 | assert_eq!(f(8, -3).round(), -3); 88 | } 89 | -------------------------------------------------------------------------------- /tests/qualifier.rs: -------------------------------------------------------------------------------- 1 | use bevy_stat_query::{ 2 | operations::StatOperation::{Add, Max, Mul}, 3 | types::StatIntPercentAdditive, 4 | Qualifier, QualifierFlag, QualifierQuery, Stat, StatMap, 5 | }; 6 | 7 | bitflags::bitflags! { 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 9 | struct Q: u32 { 10 | const Fire = 1; 11 | const Water = 2; 12 | const Earth = 4; 13 | const Air = 8; 14 | const Magic = 16; 15 | const Slash = 32; 16 | const Blast = 64; 17 | } 18 | } 19 | 20 | #[derive(Debug, Clone, Copy, Stat)] 21 | #[stat(value = "StatIntPercentAdditive")] 22 | pub struct S; 23 | 24 | #[test] 25 | pub fn qualifier_test() { 26 | let none = Qualifier::::none(); 27 | 28 | assert!(none.qualifies_as(&QualifierQuery::none())); 29 | assert!(none.qualifies_as(&QualifierQuery::Aggregate(Q::Fire))); 30 | assert!(none.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water))); 31 | assert!(none.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 32 | assert!(none.qualifies_as(&QualifierQuery::Aggregate(Q::Water | Q::Magic))); 33 | assert!(none.qualifies_as(&QualifierQuery::Aggregate(Q::Water | Q::Magic))); 34 | 35 | let fire = Qualifier::all_of(Q::Fire); 36 | assert!(!fire.qualifies_as(&QualifierQuery::none())); 37 | assert!(fire.qualifies_as(&QualifierQuery::Aggregate(Q::Fire))); 38 | assert!(fire.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water))); 39 | assert!(fire.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 40 | assert!(!fire.qualifies_as(&QualifierQuery::Aggregate(Q::Water | Q::Magic))); 41 | 42 | let fire_magic = Qualifier::all_of(Q::Fire | Q::Magic); 43 | 44 | assert!(!fire_magic.qualifies_as(&QualifierQuery::none())); 45 | assert!(!fire_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire))); 46 | assert!(!fire_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water))); 47 | assert!(fire_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 48 | assert!(fire_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water | Q::Magic))); 49 | assert!(!fire_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Water | Q::Magic))); 50 | 51 | let elemental = Qualifier::any_of(Q::Fire | Q::Water | Q::Earth | Q::Air); 52 | 53 | assert!(!elemental.qualifies_as(&QualifierQuery::none())); 54 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Fire))); 55 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Water))); 56 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Earth))); 57 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Air))); 58 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water))); 59 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Earth | Q::Air))); 60 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate( 61 | Q::Fire | Q::Water | Q::Earth | Q::Air 62 | ))); 63 | assert!(!elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Magic))); 64 | assert!(!elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Magic | Q::Blast))); 65 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 66 | assert!(elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Air | Q::Magic))); 67 | 68 | let elemental_magic = elemental.and_all_of(Q::Magic); 69 | 70 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::none())); 71 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire))); 72 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Water))); 73 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Earth))); 74 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Air))); 75 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 76 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Water | Q::Magic))); 77 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Earth | Q::Magic))); 78 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Air | Q::Magic))); 79 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Water))); 80 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Earth | Q::Air))); 81 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate( 82 | Q::Fire | Q::Water | Q::Earth | Q::Air 83 | ))); 84 | assert!(!elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Magic))); 85 | assert!(!elemental.qualifies_as(&QualifierQuery::Aggregate(Q::Magic | Q::Blast))); 86 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Magic))); 87 | assert!(elemental_magic.qualifies_as(&QualifierQuery::Aggregate(Q::Fire | Q::Air | Q::Magic))); 88 | 89 | assert!(!none.qualifies_as(&QualifierQuery::Exact { 90 | any_of: Q::none(), 91 | all_of: Q::Fire, 92 | })); 93 | 94 | assert!(!elemental.qualifies_as(&QualifierQuery::Exact { 95 | any_of: Q::none(), 96 | all_of: Q::Fire, 97 | })); 98 | 99 | assert!(fire.qualifies_as(&QualifierQuery::Exact { 100 | any_of: Q::none(), 101 | all_of: Q::Fire, 102 | })); 103 | 104 | let query_elemental = QualifierQuery::Exact { 105 | any_of: Q::Fire | Q::Water | Q::Earth | Q::Air, 106 | all_of: Q::none(), 107 | }; 108 | let all_elements = Qualifier::all_of(Q::Fire | Q::Water | Q::Earth | Q::Air); 109 | 110 | assert!(elemental.qualifies_as(&query_elemental)); 111 | assert!(!none.qualifies_as(&query_elemental)); 112 | 113 | assert!(!all_elements.qualifies_as(&query_elemental)); 114 | assert!(!fire.qualifies_as(&query_elemental)); 115 | assert!(!fire_magic.qualifies_as(&query_elemental)); 116 | 117 | let mut map = StatMap::::new(); 118 | map.insert_base(none, S, 1); 119 | map.insert_base(fire, S, 2); 120 | map.insert_base(fire_magic, S, 4); 121 | assert_eq!(map.eval_stat(&QualifierQuery::none(), &S), 1); 122 | assert_eq!(map.eval_stat(&QualifierQuery::Aggregate(Q::Fire), &S), 3); 123 | assert_eq!( 124 | map.eval_stat(&QualifierQuery::Aggregate(Q::Fire | Q::Magic), &S), 125 | 7 126 | ); 127 | 128 | let mut map = StatMap::::new(); 129 | map.modify(none, S, Add(2)); 130 | // + 100% 131 | map.modify(fire, S, Mul(100)); 132 | map.modify(fire_magic, S, Max(2)); 133 | assert_eq!(map.eval_stat(&QualifierQuery::none(), &S), 2); 134 | assert_eq!(map.eval_stat(&QualifierQuery::Aggregate(Q::Fire), &S), 4); 135 | assert_eq!( 136 | map.eval_stat(&QualifierQuery::Aggregate(Q::Fire | Q::Magic), &S), 137 | 2 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /tests/serde.rs: -------------------------------------------------------------------------------- 1 | use bevy_ecs::{component::Component, world::World}; 2 | use bevy_reflect::TypePath; 3 | use bevy_serde_lens::{BevyObject, DefaultInit, WorldExtension}; 4 | use bevy_stat_query::{ 5 | operations::StatOperation, types::*, Fraction, Qualifier, Stat, StatExtension, StatMap, 6 | }; 7 | use bevy_stat_query::{StatDeserializers, StatVTable, STAT_DESERIALIZERS}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | bitflags::bitflags! { 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, TypePath)] 12 | pub struct MyFlags: u32 { 13 | const A = 1; 14 | const B = 2; 15 | const C = 4; 16 | const D = 8; 17 | const E = 16; 18 | const F = 32; 19 | const G = 64; 20 | } 21 | } 22 | 23 | macro_rules! impl_stat { 24 | ($($name: ident: $ty: ty),* $(,)?) => { 25 | $(#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 26 | pub struct $name; 27 | 28 | impl Stat for $name { 29 | type Value = $ty; 30 | 31 | fn name(&self) -> &'static str { 32 | stringify!($name) 33 | } 34 | 35 | fn values() -> impl IntoIterator { 36 | [Self] 37 | } 38 | 39 | fn vtable() -> &'static StatVTable<$name> { 40 | static VTABLE: StatVTable<$name> = StatVTable::of::<$name>(); 41 | &VTABLE 42 | } 43 | 44 | fn as_index(&self) -> u64 { 45 | 0 46 | } 47 | 48 | fn from_index(_: u64) -> Self { 49 | Self 50 | } 51 | })* 52 | }; 53 | } 54 | 55 | impl_stat!( 56 | SInt: StatInt, 57 | SUInt: StatInt, 58 | SFloat32: StatFloat, 59 | SFlags: StatFlags, 60 | SString: Prioritized>, 61 | SIntPct: StatIntPercent, 62 | SIntFrac: StatIntRounded>, 63 | SMul: StatMult, 64 | SFracMul: StatMult> 65 | ); 66 | 67 | #[derive(Debug, Component, Serialize, Deserialize, Default, TypePath)] 68 | pub struct BaseMarker; 69 | 70 | #[derive(Debug, BevyObject)] 71 | #[serde(transparent)] 72 | pub struct Base { 73 | #[serde(skip)] 74 | marker: DefaultInit, 75 | map: StatMap, 76 | } 77 | 78 | #[derive(Debug, Component, Serialize, Deserialize, Default, TypePath)] 79 | pub struct OpMarker; 80 | 81 | #[derive(Debug, BevyObject)] 82 | #[serde(transparent)] 83 | pub struct Op { 84 | #[serde(skip)] 85 | marker: DefaultInit, 86 | map: StatMap, 87 | } 88 | 89 | #[derive(Debug, Component, Serialize, Deserialize, Default, TypePath)] 90 | pub struct FullMarker; 91 | 92 | #[derive(Debug, BevyObject)] 93 | #[serde(transparent)] 94 | pub struct Full { 95 | #[serde(skip)] 96 | marker: DefaultInit, 97 | map: StatMap, 98 | } 99 | 100 | #[test] 101 | pub fn serde_test() { 102 | let mut world = World::new(); 103 | world.register_deserialize_resource_cx::(|res, callback| { 104 | STAT_DESERIALIZERS.set(res, callback) 105 | }); 106 | world.register_stat::(); 107 | world.register_stat::(); 108 | world.register_stat::(); 109 | world.register_stat::(); 110 | world.register_stat::(); 111 | world.register_stat::(); 112 | world.register_stat::(); 113 | world.register_stat::(); 114 | world.register_stat::(); 115 | 116 | let q_false = Qualifier::all_of(false); 117 | world.spawn((BaseMarker, { 118 | let mut map = StatMap::new(); 119 | map.insert_base(q_false, SInt, -4); 120 | map.insert_base(q_false, SUInt, 7); 121 | map.insert_base(q_false, SFloat32, 3.5); 122 | map.insert_base(q_false, SFlags, MyFlags::F); 123 | map.insert_base(q_false, SString, "Ferris the Rustacean".into()); 124 | map.insert_base(q_false, SIntFrac, 69); 125 | map.insert_base(q_false, SIntPct, 420); 126 | map.insert_base(q_false, SMul, 1.5); 127 | map.insert_base(q_false, SFracMul, Fraction::new(44, 57)); 128 | map 129 | })); 130 | let value = world 131 | .save::(serde_json::value::Serializer) 132 | .unwrap(); 133 | world.despawn_bound_objects::(); 134 | world.load::(&value).unwrap(); 135 | let value2 = world 136 | .save::(serde_json::value::Serializer) 137 | .unwrap(); 138 | assert_eq!(value, value2); 139 | 140 | world.spawn((OpMarker, { 141 | let mut map = StatMap::new(); 142 | use StatOperation::*; 143 | map.modify(q_false, SInt, Add(-4)); 144 | map.modify(q_false, SUInt, Max(7)); 145 | map.modify(q_false, SFloat32, Min(3.5)); 146 | map.modify(q_false, SFlags, Or(MyFlags::F)); 147 | map.modify( 148 | q_false, 149 | SString, 150 | Or(Prioritized::new("Ferris the Rustacean".into(), 0)), 151 | ); 152 | map.modify(q_false, SIntFrac, Mul(Fraction::new(43, -47))); 153 | map.modify(q_false, SIntPct, Mul(32)); 154 | map.modify(q_false, SMul, Mul(102.125)); 155 | map.modify(q_false, SFracMul, Mul(Fraction::new(0, 1))); 156 | map 157 | })); 158 | let value = world.save::(serde_json::value::Serializer).unwrap(); 159 | world.despawn_bound_objects::(); 160 | world.load::(&value).unwrap(); 161 | 162 | let value2 = world.save::(serde_json::value::Serializer).unwrap(); 163 | assert_eq!(value, value2); 164 | 165 | world.spawn((FullMarker, { 166 | let mut map = StatMap::new(); 167 | map.insert(q_false, SInt, Default::default()); 168 | map.insert(q_false, SUInt, Default::default()); 169 | map.insert(q_false, SString, Default::default()); 170 | map.insert(q_false, SIntFrac, Default::default()); 171 | map.insert(q_false, SIntPct, Default::default()); 172 | map.insert(q_false, SFracMul, Default::default()); 173 | map.insert(q_false, SFloat32, Default::default()); 174 | map.insert(q_false, SFlags, Default::default()); 175 | map.insert(q_false, SMul, Default::default()); 176 | map 177 | })); 178 | let value = world 179 | .save::(serde_json::value::Serializer) 180 | .unwrap(); 181 | world.despawn_bound_objects::(); 182 | world.load::(&value).unwrap(); 183 | let value2 = world 184 | .save::(serde_json::value::Serializer) 185 | .unwrap(); 186 | assert_eq!(value, value2); 187 | world.despawn_bound_objects::(); 188 | 189 | world.spawn((FullMarker, { 190 | let mut map = StatMap::new(); 191 | map.insert(q_false, SInt, Default::default()); 192 | map.insert(q_false, SUInt, Default::default()); 193 | map.insert(q_false, SString, Default::default()); 194 | map.insert(q_false, SIntFrac, Default::default()); 195 | map.insert(q_false, SIntPct, Default::default()); 196 | map.insert(q_false, SFracMul, Default::default()); 197 | map.insert(q_false, SFloat32, Default::default()); 198 | map.insert(q_false, SFlags, Default::default()); 199 | map.insert(q_false, SMul, Default::default()); 200 | map 201 | })); 202 | use postcard::ser_flavors::Flavor; 203 | let mut vec = postcard::Serializer { 204 | output: postcard::ser_flavors::AllocVec::new(), 205 | }; 206 | world.save::(&mut vec).unwrap(); 207 | let result = vec.output.finalize().unwrap(); 208 | world.despawn_bound_objects::(); 209 | world 210 | .load::(&mut postcard::Deserializer::from_bytes(&result)) 211 | .unwrap(); 212 | 213 | let mut vec2 = postcard::Serializer { 214 | output: postcard::ser_flavors::AllocVec::new(), 215 | }; 216 | world.save::(&mut vec2).unwrap(); 217 | let result2 = vec2.output.finalize().unwrap(); 218 | assert_eq!(result, result2); 219 | } 220 | --------------------------------------------------------------------------------