diff --git a/flecs_ecs/examples/flecs/game_mechanics/inventory_system.rs b/flecs_ecs/examples/flecs/game_mechanics/inventory_system.rs new file mode 100644 index 00000000..84cdd575 --- /dev/null +++ b/flecs_ecs/examples/flecs/game_mechanics/inventory_system.rs @@ -0,0 +1,549 @@ +//! This example shows one possible way to implement an inventory system +//! using ECS relationships. + +use flecs_ecs::prelude::*; + +//MARK: Components + +// Inventory tags, relationships + +/// Base item type +#[derive(Component, Debug)] +struct Item; + +/// Container tag +#[derive(Component, Debug)] +struct Container; + +/// Inventory tag +#[derive(Component, Debug)] +struct Inventory; + +#[derive(Component, Debug)] +struct ContainedBy; + +// Item / unit properties + +/// Item is active/worn +#[derive(Component, Debug)] +struct Active; + +/// Number of items the instance represents +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +struct Amount { + amount: i32, +} + +/// Health of the item +#[derive(Component, Debug, Clone)] +struct Health { + value: i32, +} + +/// Amount of damage an item deals per use +#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] +struct Attack { + value: i32, +} + +// Items +#[derive(Component, Debug)] +struct Sword; +#[derive(Component, Debug)] +struct Armor; +#[derive(Component, Debug)] +struct Coin; + +// Item prefab types + +#[derive(Component, Debug)] +struct WoodenSword; +#[derive(Component, Debug)] +struct IronSword; +#[derive(Component, Debug)] +struct WoodenArmor; +#[derive(Component, Debug)] +struct IronArmor; + +//MARK: Utility Functions + +/// Find the "kind" of an item (e.g., Sword, Armor, Coin) by looking for something +/// that inherits from `Item`. +fn item_kind(item: EntityView<'_>) -> Option { + let world = item.world(); + let mut result_entity: Option = None; + + item.each_component(|id| { + if id.is_entity() { + // If id is a plain entity (component), check if component inherits + // from Item + if id.entity_view().has::<(flecs::IsA, Item)>() { + result_entity = Some(id.entity_view().id()); + } + } else if id.is_pair() { + // If item has a base entity, check if the base has an attribute + // that is an Item. + if id.first_id() == flecs::IsA::ID { + if let Some(base_kind) = item_kind(id.second_id()) { + result_entity = Some(base_kind); + } + } + } + }); + + result_entity +} + +/// Return the "name of the prefab" (e.g., WoodenSword) if found, +/// otherwise the more generic kind (e.g., Sword). +fn item_name(item: EntityView<'_>) -> Option { + let world = item.world(); + let mut result_name: Option = None; + + item.each_component(|id| { + if id.is_entity() { + if id.entity_view().has::<(flecs::IsA, Item)>() { + result_name = id.entity_view().get_name(); + } + } else if id.is_pair() && id.first_id() == flecs::IsA::ID { + if let Some(base_kind) = item_kind(id.second_id()) { + result_name = id.second_id().get_name(); + } + } + }); + + result_name +} + +/// If entity is not a Container, get its Inventory target (the actual container). +fn get_container(container: EntityView<'_>) -> Entity { + let world = container.world(); + if container.has::() { + return container.id(); + } + container.target::(0).unwrap().id() +} + +/// Iterate all items in an inventory +fn for_each_item(container: EntityView<'_>, mut func: F) +where + F: FnMut(flecs_ecs::core::EntityView<'_>, ()), +{ + let world = container.world(); + world + .query::<()>() + .with_first::(container) + .build() + .each_entity(func); +} + +/// Find item in the inventory of the specified "kind". +/// If `active_required == true`, only return items that have `Active`. +fn find_item_w_kind( + container: EntityView<'_>, + kind: Entity, + active_required: bool, +) -> Option { + let world = container.world(); + + let mut result: Option = None; + + let container = world.entity_from_id(get_container(container)); + + for_each_item(container, |item, _| { + // Check if we should only return active items. This is useful when + // searching for an item that needs to be equipped. + if active_required && !item.has::() { + return; + } + + if let Some(ik) = item_kind(item) { + if ik == kind { + result = Some(item.id()); + } + } + }); + + result +} + +/// Transfer a single item to a different container. +fn transfer_item(container: EntityView<'_>, item: EntityView<'_>) { + let world = container.world(); + + let amt = item.try_cloned::<&Amount>().unwrap_or(Amount { amount: 1 }); + + if amt.amount > 0 { + // If item has amount we need to check if the container already has an + // item of this kind, and increase the value. + let ik = item_kind(item).unwrap(); + let dst_item = find_item_w_kind(container, ik, false); + + if let Some(dst_item) = dst_item { + // If a matching item was found, increase its amount + world + .entity_from_id(dst_item) + .get::<&mut Amount>(|dst_amt| { + dst_amt.amount += amt.amount; + }); + item.destruct(); + return; + } else { + // If no matching item was found, fallthrough which will move the + // item from the src container to the dst container + } + } + + // Move item to target container (replaces previous ContainedBy, if any) + item.add_first::(container); +} + +/// Move all items from `src` container to `dst` container. +fn transfer_items(dst: EntityView<'_>, src: EntityView<'_>) { + let world = dst.world(); + println!(">> Transfer items from {} to {}\n", src.name(), dst.name()); + + // Defer is recommended in Flecs so you can safely modify ECS while iterating. + // In flecs-rust you can do `world.defer(|w| { ... })`. + world.defer(|| { + let dst_container = world.entity_from_id(get_container(dst)); + let src_container = world.entity_from_id(get_container(src)); + + for_each_item(src_container, |item, _| { + transfer_item(dst_container, item); + }); + }); +} + +/// Attack `player` with `weapon`. +fn attack(player: EntityView<'_>, weapon: EntityView<'_>) { + let world = player.world(); + + println!( + ">> {} is attacked with a {}!", + player.name(), + item_name(weapon).unwrap_or("UnknownItem".to_string()) + ); + + let atk = weapon.try_cloned::<&Attack>(); + + if atk.is_none() { + // A weapon without Attack power? Odd. + println!(" - the weapon is a dud"); + return; + } + + let mut att_value = atk.unwrap().value; + + // Get armor item, if player has equipped any + if let Some(armor_e) = find_item_w_kind(player, world.component_id::(), true) { + let armor_e = world.entity_from_id(armor_e); + armor_e.get::>(|health| { + if let Some(armor_health) = health { + println!( + " - {} defends with {} ({} health)", + player.name(), + item_name(armor_e).unwrap(), + armor_health.value + ); + + // Subtract attack from armor health. If armor health goes below + // zero, delete the armor and carry over remaining attack points. + armor_health.value -= att_value; + + if armor_health.value <= 0 { + att_value = -armor_health.value; + armor_e.destruct(); + println!(" - {} is destroyed!", item_name(armor_e).unwrap()); + } else { + println!( + " - {} has {} health left after taking {} damage", + item_name(armor_e).unwrap(), + armor_health.value, + att_value + ); + att_value = 0; + } + } else { + // Armor without Defense power? Odd. + println!(" - the {} armor is a dud", item_name(armor_e).unwrap()); + } + }); + } else { + // Brave but stupid + println!(" - {} fights without armor!", player.name()); + } + + // For each usage of the weapon, subtract one from its health + weapon.get::<&mut Health>(|weapon_health| { + if weapon_health.value > 0 { + weapon_health.value -= 1; + if weapon_health.value == 0 { + println!(" - {} is destroyed!", item_name(weapon).unwrap()); + weapon.destruct(); + } else { + println!( + " - {} has {} uses left", + item_name(weapon).unwrap(), + weapon_health.value + ); + } + } + }); + + // If armor didn't counter the whole attack, subtract from the player health + if att_value > 0 { + player.get::<&mut Health>(|player_health| { + player_health.value -= att_value; + if player_health.value <= 0 { + println!(" - {} died!", player.name()); + player.destruct(); + } else { + println!( + " - {} has {} health left after taking {} damage", + player.name(), + player_health.value, + att_value + ); + } + }); + } + + println!(); +} + +/// Print items in a container / inventory. +fn print_items(container: EntityView<'_>) { + println!("-- {}'s inventory:", container.name()); + + let world = container.world(); + let mut count = 0; + + // In case the player entity was provided, make sure we're working + // with its inventory entity. + let container = world.entity_from_id(get_container(container)); + + for_each_item(container, |item, _| { + // Items with an Amount component fill up a single inventory slot but + // represent multiple instances, like coins. + let amount = item + .try_cloned::<&Amount>() + .unwrap_or(Amount { amount: 1 }) + .amount; + println!( + " - {} {} ({})", + amount, + item_name(item).unwrap_or("UnknownItem".to_string()), + world.entity_from_id(item_kind(item).unwrap()).name() + ); + + count += 1; + }); + + if count == 0 { + println!(" - << empty >>"); + } + + println!(); +} + +//MARK: ECS Modules + +#[derive(Component)] +pub struct InventoryComponentsModule; + +#[derive(Component)] +pub struct InventoryModule; + +impl Module for InventoryComponentsModule { + fn module(world: &World) { + world.module::("inventory::components"); + + world.component::(); + world.component::(); + world.component::(); + // Item can only be contained by one container + world + .component::() + .add_trait::(); + } +} + +impl Module for InventoryModule { + fn module(world: &World) { + world.module::("inventory::systems"); + world.import::(); + } +} + +#[derive(Component)] +pub struct ItemComponentsModule; + +impl Module for ItemComponentsModule { + fn module(world: &World) { + world.module::("item::components"); + + world.component::(); + world.component::(); + + //health gets copied to instance, don't share, defaults to Onstantiate Copy, + world.component::(); + + world + .component::() + .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>(); + + world + .component::() + .is_a::() + .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>(); + + world + .component::() + .is_a::() + .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>(); + world + .component::() + .is_a::() + .add_trait::<(flecs::OnInstantiate, flecs::Inherit)>(); + + //register item prefabs + world + .prefab_type::() + .add::() + .set(Attack { value: 1 }) + // copy to instance, don't share + .set(Health { value: 5 }); + + world + .prefab_type::() + .add::() + .set(Attack { value: 4 }) + .set(Health { value: 10 }); + + world + .prefab_type::() + .add::() + .set(Health { value: 10 }); + + world + .prefab_type::() + .add::() + .set(Health { value: 20 }); + } +} + +#[derive(Component)] +pub struct ItemModule; + +impl Module for ItemModule { + fn module(world: &World) { + world.module::("item::systems"); + world.import::(); + } +} + +//MARK: Main + +#[test] +fn main() { + let mut world = World::new(); + + world.import::(); + world.import::(); + + // Create a loot box with items + let loot_box = world + .entity_named("Chest") + .add::() + .with_first::(|| { + world.entity().is_a::(); + world.entity().is_a::(); + world.entity().add::().set(Amount { amount: 30 }); + }); + + // Create a player entity with an inventory + let player = world + .entity_named("Player") + .set(Health { value: 10 }) + .add_first::(world.entity().add::().with_first::( + || { + world.entity().add::().set(Amount { amount: 20 }); + }, + )); + + // Print items in loot box + print_items(loot_box); + + // Print items in player inventory + print_items(player); + + // Copy items from loot box to player inventory + transfer_items(player, loot_box); + + // Print items in player inventory after transfer + print_items(player); + + // Print items in loot box after transfer + print_items(loot_box); + + // Find armor entity & equip it + if let Some(armor) = find_item_w_kind(player, world.component_id::(), false) { + world.entity_from_id(armor).add::(); + } + + // Create a weapon to attack the player with + let my_sword = world.entity().is_a::(); + + // Attack player + attack(player, my_sword); + attack(player, my_sword); + attack(player, my_sword); + attack(player, my_sword); + attack(player, my_sword); + + // Output: + // -- Chest's inventory: + // - 1 IronSword (Sword) + // - 1 WoodenArmor (Armor) + // - 30 Coin (Coin) + + // -- Player's inventory: + // - 20 Coin (Coin) + + // >> Transfer items from Chest to Player + + // -- Player's inventory: + // - 50 Coin (Coin) + // - 1 IronSword (Sword) + // - 1 WoodenArmor (Armor) + + // -- Chest's inventory: + // - << empty >> + + // >> Player is attacked with a IronSword! + // - Player defends with WoodenArmor (10 health) + // - WoodenArmor has 6 health left after taking 4 damage + // - IronSword has 9 uses left + + // >> Player is attacked with a IronSword! + // - Player defends with WoodenArmor (6 health) + // - WoodenArmor has 2 health left after taking 4 damage + // - IronSword has 8 uses left + + // >> Player is attacked with a IronSword! + // - Player defends with WoodenArmor (2 health) + // - WoodenArmor is destroyed! + // - IronSword has 7 uses left + // - Player has 8 health left after taking 2 damage + + // >> Player is attacked with a IronSword! + // - Player fights without armor! + // - IronSword has 6 uses left + // - Player has 4 health left after taking 4 damage + + // >> Player is attacked with a IronSword! + // - Player fights without armor! + // - IronSword has 5 uses left + // - Player died! +} diff --git a/flecs_ecs/examples/flecs/game_mechanics/mod.rs b/flecs_ecs/examples/flecs/game_mechanics/mod.rs new file mode 100644 index 00000000..b8bf03e5 --- /dev/null +++ b/flecs_ecs/examples/flecs/game_mechanics/mod.rs @@ -0,0 +1 @@ +mod inventory_system; diff --git a/flecs_ecs/examples/flecs/z_ignore_main_test.rs b/flecs_ecs/examples/flecs/z_ignore_main_test.rs index 0001c5d2..4bf43551 100644 --- a/flecs_ecs/examples/flecs/z_ignore_main_test.rs +++ b/flecs_ecs/examples/flecs/z_ignore_main_test.rs @@ -17,6 +17,7 @@ fn init() { pub mod z_ignore_test_common; mod entities; +mod game_mechanics; mod hello_world; mod observers; mod prefabs;