diff --git a/docs/build/tutorials/howto-create_farming_game/Part1.md b/docs/build/tutorials/howto-create_farming_game/Part1.md new file mode 100644 index 0000000..d1f85fc --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part1.md @@ -0,0 +1,80 @@ +Part 1. How to make a game on WAX. General concepts. +==================================================== + +Hey WAX game makers! Ready for a step-by-step guide to building your own game on WAX? Let’s dive in: + +* **Atomic Assets Setup**: Your toolkit for creating everything cool in the game. + +* **Game’s Main Entities**: From farming items and avatars to user profiles – we’ve got you covered. + +* **Random Function Usage on WAX**: Add a twist of unpredictability to your game. + +* **Resources and Tokens**: The backbone of your game’s economy. + +* **NFT and Token Staking**: Let players invest and earn in your game. + +* **Farming of Resources**: The heart of resource management. + +* **Blends and Upgrades**: Keep players engaged with evolving gameplay. + +* **DAO and Governance**: Give your community a voice. + +* **Leaderboards**: Fuel the competitive spirit. + +* **Quest Systems**: Keep the adventure going. + + +These essentials are your ticket to building a standout resource farming game on WAX, complete with play2earn excitement. Let’s turn those creative ideas into a gaming reality! + +Ready for a deep dive into the building blocks of your WAX game? Let’s break it down: + +* **Articles 1 & 2**: We’ll tackle gaming objects like farming items and farmable objects. Imagine a pickaxe, a classic tool, slotting into a stonehouse. We’ll explore how these items interact and their unique properties in resource and token farming. + +* **Article 3**: It’s all about resources and tokens. We’re redefining resources as in-game elements exclusive to the game’s economy. As a dev, you’ll learn to navigate which items to use and how. + +* **Article 4**: Let’s talk NFT staking. It’s an optional but savvy move to ‘freeze’ assets in a smart contract, steering clear of open markets to boost NFT value. We’ll cover how staking keeps your NFT data in-house, ditching the need for third-party tracking. + + +**Article 5** takes you into the world of farming types and processes – it’s a bit complex, but we’ve got it broken down line by line. Think of farming as harvesting tokens or resources from something like a house, planet, or land plot. + +Considering all above our game example can look like this: + +![](/public/assets/images/tutorials/howto-create_farming_game/part1/Farming-screen-980x551.jpg) + +That wraps up our introductory series. Next, we’ll dive into game progression, upgrades, and settings, topped off with the UI part of your game. It’s about taking your game from basic to brilliant. Get ready for a deeper understanding of making your game truly engaging. + +**Article 1** dives into blends, a cool way of merging two items into a new one by specific rules. We’re providing a versatile code snippet, perfect for NFT blends in games or gamifying NFT collections. + +In **Article 2**, we explore upgrades, a twist on blends where you enhance items by changing attributes. The magic lies in setting rules either through NFTs or game logic. Upgrades in our game boost mining rates. + +**Article 3** focuses on avatars, more than just looks – they’re game changers affecting mining rates and more. We’ll discuss how avatars, equipped with unique items, spice up the gameplay and the NFT market. + +**The 4th** article covers game profiles, essential data storage considerations on the blockchain, even as basic as a username. + +Finally, **Article 5** tackles UI with ReactJS, teaching you to interact with smart contracts and create a basic game UI. + +The upcoming section is all about DAO and governance in your game. + +* **Article 1** tackles swapping resources, a twist on the farming process. It’s about turning resources into tokens, with your own rules for pools, dynamic exchange rates, and even a taxation system. + +* **Article 2** dives into token staking and basic DAO mechanics. Learn how staking tokens can grant voting power and influence game settings. + +* **The third article** discusses the proposal system, focusing on in-game voting impacting game dynamics and tokenomics. + +* **The final article** will zoom in on the user interfaces for the features discussed, particularly around DAO and governance. + + +The next block is all about game progression and leaderboards. + +* **Leaderboards**: They’re simple yet varied. We’ll show you how to set up different types, like the top miners of specific resources within a set time frame. + +* **Quests**: This is more complex, with diverse and dynamic possibilities. Expect general systems and examples like daily logins, staking a certain amount of tokens, or resource exchanges. Quests, like leaderboards, offer rewards for completion. + +* **The final article** focuses on UI design for quests and leaderboards, complete with practical code examples. + +* The last article in this section will delve into the UI aspects of quests and leaderboards, complete with practical code examples to guide you. + + +Wrapping up the series, we’ll bring the whole game together, covering the build and test processes. Remember, while we focus on a fully on-chain game, integration with centralized components for elements like fast PVP battles is possible. This allows for a hybrid approach, combining the strengths of both decentralized and centralized gaming aspects. + +P.S. Keep in mind that this article and the accompanying framework may evolve as development progresses. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part10.md b/docs/build/tutorials/howto-create_farming_game/Part10.md new file mode 100644 index 0000000..daee5f0 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part10.md @@ -0,0 +1,661 @@ +Part 10: Implementing avatars in WAX games +========================================== + +This article will explore how to create avatars and their equipment, focusing on the customization and personalization aspects that enhance player experience. By detailing the process of designing avatars and selecting their gear, we aim to provide insights into building more engaging and interactive game elements, allowing players to deeply immerse themselves in the game world with characters that reflect their style and preferences. +### 1. Creating categories + +Creating an avatar category involves defining a character with specific characteristics, which can later be enhanced by equipping items. This foundational step allows for the customization of avatars, providing players with the ability to tailor characters to their play style and preferences, thus enriching the gaming experience by adding depth to character development and interaction within the game world. + +![](/public/assets/images/tutorials/howto-create_farming_game/part10/image1.png) + +| Attribute Name | Attribute Type | Description | +|----------------|----------------|--------------------------------------------------| +| name | string | Avatar’s name | +| description | string | Description of avatar’s lore and properties | +| img | image | Image of the avatar | +| economic | uint32 | Reduces the price of upgrade | +| productivity | uint32 | Increases mining rate | +| vitality | uint32 | Increases upgrade percentage | +| bravery | uint32 | Will affect quests | +| diplomacy | uint32 | Will affect interactions with other players | + +Table. 1 – attributes of “avatar” + + + +Creating a category for equipment items mirrors the process of avatar creation, with each piece of equipment also possessing distinct characteristics. + +![](/public/assets/images/tutorials/howto-create_farming_game/part10/image2.png) + +| Attribute Name | Attribute Type | Description | +|----------------|----------------|-------------------------------------------------------| +| name | string | Item’s name | +| description | string | Description of item’s lore and properties | +| img | image | Image of the item | +| type | string | Represents the type of item (jewelry, flag, crown etc.)| +| economic | uint32 | Reduces the price of upgrade | +| productivity | uint32 | Increases mining rate | +| vitality | uint32 | Increases upgrade percentage | +| bravery | uint32 | Will affect quests | +| diplomacy | uint32 | Will affect interactions with other players | + +Table. 2 – attributes of “equip” +### 2. Creating templates + +Here is an example of creating an avatar and an item of equipment. + +![](/public/assets/images/tutorials/howto-create_farming_game/part10/image3.png) + +![](/public/assets/images/tutorials/howto-create_farming_game/part10/image4.png) + +The minting of avatars and equipment follows the processes outlined in previous articles, involving the creation and registration of these elements on the blockchain. +### 3. Adding new tables to the contract code + +Adding a table that links each player with their active avatar and equipped items is a strategic development step. This table not only tracks which avatars and items are currently in use but also facilitates interactions within the game, such as battles or resource collection, based on the equipped items’ attributes. + +```C +struct [[eosio::table]] avatars_j + { + name owner; + std::vector equipment; + + + uint64_t primary_key() const { return owner.value; } + }; + typedef multi_index< "avatarsc"_n, avatars_j> avatars_t; +``` + +owner – the account that puts on the avatar and equipment + +equipment – a vector of uint64_t – identifiers that indicate the active avatar and equipment + +Creating a table for player stats involves aggregating the attributes of the avatar and any equipped items to reflect the player’s current capabilities within the game. + +```C +struct [[eosio::table]] stats_j + { + name owner; + std::map stats; + + + uint64_t primary_key() const {return owner.value;} + }; + typedef multi_index<"stats"_n, stats_j> stats_t; +``` + +owner – an account whose characteristics are specified in the table + +stats – a map containing characteristics in the format `{“economic” : 10, “bravery” : 7, etc}` + +### 4. The logic for setting avatars and equipment. + +The logic for setting avatars and equipment. in the game involves players selecting their character and outfitting them with various items to enhance their stats.  + +```C + else if (memo == "set avatar") + { + check(asset_ids.size() == 1, "You must transfer only one avatar"); + set_avatar(from, asset_ids[0]); + } + else if (memo == "set equipment") + { + check(asset_ids.size() <= 4, "You can wear only 4 different equipment types at once"); + set_equipment_list(from, asset_ids); + } +``` + +Incorporating two memo options into the `receive_asset_transfer()` function allows players to either set their avatar by transferring a single avatar NFT with the memo "set avatar" or equip up to four different items by specifying "**set equipment**". The function then assigns the transferred avatar asset with the specified `asset_id` to the user's owner record, effectively updating the player's character or equipment setup in the game. + +```C +void game::set_avatar(const name &owner, const uint64_t &asset_id) +{ + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); + + check(asset_itr->collection_name == "collname"_n, "Wrong collection"); + check(asset_itr->schema_name == "avatar"_n, "Not an avatar asset"); + + avatars_t avatars_table(get_self(), get_self().value); + auto owner_avatar_itr = avatars_table.find(owner.value); + + if (owner_avatar_itr == std::end(avatars_table)) + { + avatars_table.emplace(get_self(), [&](auto &new_row) + { + new_row.owner = owner; + new_row.equipment.resize(5); + new_row.equipment[0] = asset_id; }); + } + else + { + // should return avatar asset back to player + const uint64_t old_avatar_id = owner_avatar_itr->equipment[0]; + + const std::vector assets_to_transfer = {old_avatar_id}; + const std::string memo = "return avatar"; + + action( + permission_level{get_self(), "active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "transfer"_n, + std::make_tuple( + get_self(), + owner, + assets_to_transfer, + memo)) + .send(); + + avatars_table.modify(owner_avatar_itr, get_self(), [&](auto &row) + { row.equipment[0] = asset_id; }); + } + + recalculate_stats(owner); +} +``` + +1. The function's purpose is to validate the asset transferred by the player, ensuring it belongs to the correct collection and category for avatars or equipment.  + +```Cpp + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); + + check(asset_itr->collection_name == "collname"_n, "Wrong collection"); + check(asset_itr->schema_name == "avatar"_n, "Not an avatar asset"); +``` + +2\. To update a player's avatar in the game, the function retrieves the player's information from the avatars table using their username. + +```Cpp +avatars_t avatars_table(get_self(), get_self().value); + auto owner_avatar_itr = avatars_table.find(owner.value); +``` + +3\. If the user does not already exist in the avatars table, the function adds them by setting up a vector with five elements initialized to zeros. The avatar's ID is then placed in the first position of this vector, effectively registering the new avatar under the player's username.  + +```Cpp +if (owner_avatar_itr == std::end(avatars_table)) + { + avatars_table.emplace(get_self(), [&](auto &new_row) + { + new_row.owner = owner; + new_row.equipment.resize(5); + new_row.equipment[0] = asset_id; }); + } +``` + +4\. If the player already exists in the avatars table, the function updates their avatar with the new one provided in the argument. The old avatar is then returned to the player via an atomicassets::transfer, ensuring the player retains ownership of their previous avatar. + +```Cpp +else + { + // should return avatar asset back to player + const uint64_t old_avatar_id = owner_avatar_itr->equipment[0]; + + const std::vector assets_to_transfer = {old_avatar_id}; + const std::string memo = "return avatar"; + + action( + permission_level{get_self(), "active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "transfer"_n, + std::make_tuple( + get_self(), + owner, + assets_to_transfer, + memo)) + .send(); + + avatars_table.modify(owner_avatar_itr, get_self(), [&](auto &row) + { row.equipment[0] = asset_id; }); + } +``` + +5\. Now we need to recalculate new characteristics + +```Cpp + recalculate_stats(owner); +``` + +The function for equipping items involves listing the asset IDs of equipment to be worn by the player's avatar. This process checks each item for compatibility with the avatar and updates the player's equipment list in the game's database. + +```Cpp +void game::set_equipment_list(const name &owner, +const std::vector &asset_ids) +{ + std::vector assets_to_return; + + std::map equiped_types; + equiped_types.insert(std::pair("flag", 0)); + equiped_types.insert(std::pair("jewelry", 0)); + equiped_types.insert(std::pair("crown", 0)); + equiped_types.insert(std::pair("cloak", 0)); + + for (uint64_t asset_id : asset_ids) + { + set_equipment_item(owner, asset_id, assets_to_return, equiped_types); + } + + const std::string memo = "return equipment"; + + action( + permission_level{get_self(), "active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "transfer"_n, + std::make_tuple( + get_self(), + owner, + assets_to_return, + memo)) + .send(); + + recalculate_stats(owner); +} +``` + +Function description: + +1\. **Prepare for Changes:** Create a vector to hold asset IDs of equipment to be returned and a map to ensure each equipment type is equipped no more than once. + +```Cpp + std::vector assets_to_return; + + std::map equiped_types; + equiped_types.insert(std::pair("flag", 0)); + equiped_types.insert(std::pair("jewelry", 0)); + equiped_types.insert(std::pair("crown", 0)); + equiped_types.insert(std::pair("cloak", 0)); +``` + +**2\. Equip New Items:** Iterate through the provided asset IDs, equipping each item while adhering to the rule that each equipment type can only be worn once. + +```Cpp + for (uint64_t asset_id : asset_ids) + { + set_equipment_item(owner, asset_id, assets_to_return, equiped_types); + } +``` + +**3\. Update and Return:** Return any old assets back to the player's inventory and recalculate the player's characteristics based on the new equipment setup to reflect the changes in the player's abilities or stats accurately. + +```Cpp +const std::string memo = "return equipment"; + + action( + permission_level{get_self(), "active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "transfer"_n, + std::make_tuple( + get_self(), + owner, + assets_to_return, + memo)) + .send(); + + recalculate_stats(owner); +``` + +Let's consider the functions of putting on a single item **asset_id** on the player owner. **assets_to_return** stores the assets to be returned, **equiped_types** stores the number of worn items of each type of equipment. + +```Cpp +void game::set_equipment_item(const name &owner, const uint64_t asset_id, +std::vector &assets_to_return, std::map &equiped_types) +{ + avatars_t avatars_table(get_self(), get_self().value); + + auto owner_avatar_itr = avatars_table.find(owner.value); + check(owner_avatar_itr != std::end(avatars_table), "You can put equipment only when you have an avatar"); + + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); + auto equipment_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + + check(asset_itr->collection_name == "collname"_n, "Wrong collection"); + check(asset_itr->schema_name == "equip"_n, "Not an equipment item"); + + uint32_t position = 0; + const std::string type = std::get(equipment_template_idata["type"]); + + equiped_types[type]++; + check(equiped_types[type] <= 1, "You can wear only 4 different euipment types at once"); + + if (type == "flag") + { + position = 1; + } + else if (type == "jewelry") + { + position = 2; + } + else if (type == "crown") + { + position = 3; + } + else if (type == "cloak") + { + position = 4; + } + else + { + check(false, "Wrong type of equipment"); + } + + const uint64_t old_equip_id = owner_avatar_itr->equipment[position]; + + if (old_equip_id != 0) + { + assets_to_return.push_back(old_equip_id); + } + + avatars_table.modify(owner_avatar_itr, get_self(), [&](auto &row) + { row.equipment[position] = asset_id; }); +} +``` + +Function description: + +1\. Verify the player's presence in the equipment and avatar table; terminate if absent. Ensure the asset, from the correct collection and type, points correctly in the asset table. Load immutable template data into `equipment_template_idata`. + +```Cpp + avatars_t avatars_table(get_self(), get_self().value); + + auto owner_avatar_itr = avatars_table.find(owner.value); + check(owner_avatar_itr != std::end(avatars_table), "You can put equipment only when you have an avatar"); + + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); + auto equipment_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + + check(asset_itr->collection_name == "collname"_n, "Wrong collection"); + check(asset_itr->schema_name == "equip"_n, "Not an equipment item"); +``` + +2\. Identify the equipment type, incrementing its count in the map. If it's a duplicate, issue an error. Determine the `position` variable based on the equipment type for the new ID's placement. + +```Cpp +int32_t position = 0; + const std::string type = std::get(equipment_template_idata["type"]); + + equiped_types[type]++; + check(equiped_types[type] <= 1, "You can wear only 4 different euipment types at once"); + + if (type == "flag") + { + position = 1; + } + else if (type == "jewelry") + { + position = 2; + } + else if (type == "crown") + { + position = 3; + } + else if (type == "cloak") + { + position = 4; + } + else + { + check(false, "Wrong type of equipment"); + } +``` + +3\. If an existing item is found at the position, add its ID to the return vector. Update the table with the new asset. + +```Cpp + const uint64_t old_equip_id = owner_avatar_itr->equipment[position]; + + if (old_equip_id != 0) + { + assets_to_return.push_back(old_equip_id); + } + + avatars_table.modify(owner_avatar_itr, get_self(), [&](auto &row) + { row.equipment[position] = asset_id; }); +``` + +4\. Recalculate player characteristics based on the new equipment setup. + +```Cpp +void game::recalculate_stats(const name &owner) +{ + stats_t stats_table(get_self(), get_self().value); + auto stats_itr = stats_table.find(owner.value); + + std::map stats; + + // init stats + stats.insert(std::pair("economic", 0)); + stats.insert(std::pair("productivity", 0)); + stats.insert(std::pair("vitality", 0)); + stats.insert(std::pair("bravery", 0)); + stats.insert(std::pair("diplomacy", 0)); + + // read stats + avatars_t avatars_table(get_self(), get_self().value); + auto avatar_itr = avatars_table.require_find(owner.value, "Your avatar was deleted"); + + auto assets = atomicassets::get_assets(get_self()); + + for (uint64_t asset_id : avatar_itr->equipment) + { + if (asset_id == 0) + { + continue; + } + + auto asset_itr = assets.find(asset_id); + auto equipment_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + + for (auto &key_value_pair : stats) + { + if (equipment_template_idata.find(key_value_pair.first) != std::end(equipment_template_idata)) + { + key_value_pair.second += std::get(equipment_template_idata[key_value_pair.first]); + } + } + } + + if (stats_itr == std::end(stats_table)) + { + stats_table.emplace(get_self(), [&](auto &new_row) + { + new_row.owner = owner; + new_row.stats = stats; }); + } + else + { + stats_table.modify(stats_itr, get_self(), [&](auto &row) + { row.stats = stats; }); + } +} +``` + +**The function for calculating player characteristics involves several key steps:** + +1\. Retrieve the player's stats and avatars from their respective tables, initializing characteristics to zero. + +```Cpp + stats_t stats_table(get_self(), get_self().value); + auto stats_itr = stats_table.find(owner.value); + + std::map stats; + + // init stats + stats.insert(std::pair("economic", 0)); + stats.insert(std::pair("productivity", 0)); + stats.insert(std::pair("vitality", 0)); + stats.insert(std::pair("bravery", 0)); + stats.insert(std::pair("diplomacy", 0)); + + // read stats + avatars_t avatars_table(get_self(), get_self().value); + auto avatar_itr = avatars_table.require_find(owner.value, "Your avatar was deleted"); +``` + +2. Process active assets, skipping any with an ID of 0, and read template data for each asset. + +```Cpp + auto assets = atomicassets::get_assets(get_self()); + + for (uint64_t asset_id : avatar_itr->equipment) + { + if (asset_id == 0) + { + continue; + } + + auto asset_itr = assets.find(asset_id); + auto equipment_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + +``` + +3\. We go through all the characteristics that we want to calculate, and they are listed in the map. If a given characteristic is present in a given item, we add it to the total value + +```Cpp +for (auto &key_value_pair : stats) + { + if (equipment_template_idata.find(key_value_pair.first) != std::end(equipment_template_idata)) + { + key_value_pair.second += std::get(equipment_template_idata[key_value_pair.first]); + } + } +``` + +key_value_pair are pairs of the type {"economic", 0}, {"bravery", 0}, etc. If this element has the "economic" characteristic with a value of 3, then after this code, the "economic" field in the stats map will be 0 + 3 = 3. + +4\. Sum up the values for each characteristic listed in the map, adding to the total if present in the item. + +```Cpp + if (stats_itr == std::end(stats_table)) + { + stats_table.emplace(get_self(), [&](auto &new_row) + { + new_row.owner = owner; + new_row.stats = stats; }); + } + else + { + stats_table.modify(stats_itr, get_self(), [&](auto &row) + { row.stats = stats; }); + } +``` + +5\. Update the player's characteristics in the table if they exist; otherwise, add the player with the new stats. + +Let's describe the lines that have been added or changed in the functions. + +**Claim:** + +```Cpp +std::map stats = get_stats(owner); +``` + +This is where we get the actual characteristics of the player. + +```Cpp +const uint8_t upgrade_percentage = 2 + stats["vitality"] / 10.0f; + + const std::pair item_reward = claim_item(assets_itr, upgrade_percentage, time_now, stats); +``` + +Now upgrade_percentage is not a constant, but depends on the "vitality" characteristic. The claim_item function now accepts stats to avoid doing unnecessary calculations inside. + +**Claim_item:** + +```Cpp +float miningRate_according2lvl = miningRate + stats.at("productivity") / 10.0f; +``` + +and miningRate now also depends on the "productivity" characteristic + +**Upgradeitem:** + +```Cpp +std::map stats = get_stats(owner); + const uint8_t upgrade_percentage = 2 + stats["vitality"] / 10.0f; + const std::pair item_reward = claim_item(asset_itr, upgrade_percentage, time_now, stats); +``` + +In order to read characteristics, calculate upgrade_percentage, call the updated claim_item + +```Cpp +upgrade_item(asset_itr, upgrade_percentage, owner, next_level, time_now, stats); +``` + +and upgrade_item now accepts stats to avoid unnecessary calculations. + +**upgrade_item:** + +```Cpp +float miningRate_according2lvl = mining_rate + stats.at("productivity") / 10.0f; +``` + +Here we get updated miningRate + +```Cpp +const float &resource_price = upgrade_time * miningRate_according2lvl * (1.0f - stats.at("economic") / 100.0f); +``` + +and resource_price now decreases with the growth of the "economic" characteristic. + +Consider the get_stats function, which returns a map with player characteristics. + +```Cpp +std::map game::get_stats(const name &owner) +{ + std::map stats; + stats_t stats_table(get_self(), get_self().value); + auto stats_itr = stats_table.find(owner.value); + + if (stats_itr == std::end(stats_table)) + { + stats.insert(std::pair("economic", 0)); + stats.insert(std::pair("productivity", 0)); + stats.insert(std::pair("vitality", 0)); + stats.insert(std::pair("bravery", 0)); + stats.insert(std::pair("diplomacy", 0)); + } + else + { + stats = stats_itr->stats; + } + + return stats; +} +``` + +To calculate player characteristics: + +1\. A map is created to hold the function's results. A pointer is then set to the player's entry in the stats table using their name. + +```Cpp + std::map stats; + stats_t stats_table(get_self(), get_self().value); + auto stats_itr = stats_table.find(owner.value); +``` + +2\. If the player isn't found in the table, the function returns a map with all characteristics set to zero. If the player is found, it retrieves and returns their stats from the table, effectively summarizing their current game attributes. + +```Cpp + if (stats_itr == std::end(stats_table)) + { + stats.insert(std::pair("economic", 0)); + stats.insert(std::pair("productivity", 0)); + stats.insert(std::pair("vitality", 0)); + stats.insert(std::pair("bravery", 0)); + stats.insert(std::pair("diplomacy", 0)); + } + else + { + stats = stats_itr->stats; + } + + return stats; +``` + +This article delves into creating and managing avatars and their equipment in a game, outlining the process from initial avatar category creation to the dynamic assignment of equipment. It covers the integration of avatars with in-game mechanics, such as staking and claiming rewards, and emphasizes the importance of customization in enhancing player experience.  + +The article also discusses the technical aspects of setting up and updating player stats based on equipped items, ensuring a rich and interactive gaming environment. + +**PS.** The [Following link](https://github.com/dapplicaio/GameAvatars) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part11.md b/docs/build/tutorials/howto-create_farming_game/Part11.md new file mode 100644 index 0000000..434c753 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part11.md @@ -0,0 +1,280 @@ +Part 11: UI for Blends, upgrades and avatars +============================================ + +Building on our ReactJS and WAX smart contract interaction guide, this article advances our application's development. We'll delve into the intricacies of blends and workspace upgrades, alongside the necessary tools. Additionally, guidance will be provided on interface actions in sync with our smart contract, enhancing the application's functionality and user experience. + +**Blending assets** +------------------- + +Blends in smart contracts allow collection owners to create new, improved assets by combining existing ones. Imagine upgrading your tools by blending two of the same type, resulting in a tool that extracts significantly more resources.  + +This process enriches the user experience by adding depth to resource management and gameplay strategy, seamlessly integrated into the UI for easy user interaction. + +This is what it looks like. + +![](/public/assets/images/tutorials/howto-create_farming_game/part11/image1.png) + +The smart contract features predefined blend recipes that users can utilize through the `fetchBlends` function. This functionality enables the extraction of various mix recipes from the contract, allowing users to create enhanced tools and assets by following specific blend instructions directly within the application's interface. + +```Js +export const fetchBlends = async () => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: 'dappgamemine', + table: "blendrecipes" + }); + + const templateOneRequests = rows.map(row => axios.get(`https://test.wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=minersgamers&template_id=${row.blend_components}`)); + + const responsesOne = await Promise.all(templateOneRequests); + + const templateTwoRequests = rows.map(row => axios.get(`https://test.wax.api.atomicassets.io/atomicassets/v1/templates?collection_name=minersgamers&template_id=${row.resulting_item}`)); + + const responsesTwo = await Promise.all(templateTwoRequests); + + const updatedRows = rows.map((row, index) => { + const { data:dataOne } = responsesOne[index]; + const { data:dataTwo } = responsesTwo[index]; + return { ...row, blend_components: dataOne.data, resulting_item: dataTwo.data[0] }; + }) + + return updatedRows; + } +``` + +- Inside the function, it first fetches rows of data from a specified table (blendrecipes) using the fetchRows function. +- Once the rows have been fetched, the function proceeds to create an array of requests to fetch data from an API using Axios. It iterates over each row and constructs a URL to fetch data relating to the blend components and resulting elements. +- These requests are then made concurrently using Promise.all, which waits for all requests to be resolved. +- The responses from these queries are stored in two separate arrays (responsesOne and responsesTwo). +- Having obtained the responses for the blend components and resulting elements, the function maps over the original rows again. +- Within the map function, it extracts the relevant data (dataOne and dataTwo) from the corresponding responses. +- Finally, it creates updated rows by merging the original row data with the retrieved blend component and resultant item data. + +We have also implemented a function that checks whether the user can make a blend with the available inventory according to one of the recipes. We check this using the following function: + +```js +const blendComponentsMatch = userToolAsset.some(asset => asset.template.template_id === item.blend_components[0].template_id || asset.template.template_id === item.blend_components[1].template_id); + +const isAvailable = blendComponentsMatch && +userToolAsset.some(asset => Number(asset.template.template_id) !== Number(item.blend_components[0].template_id) && Number(asset.template.template_id) !== Number(item.blend_components[1].template_id)); +``` + +- **userToolAsset** -- array of user tools; +- **item** -- each element of the array containing the recipes; + +If the function isAvailable is true, the user can make a blend; if false, the user does not have enough tools for a particular recipe. + +Now let's look at how the UI interacts with the smart contract to create a blend. + +```js + +export const blend = async ({ activeUser, componentIds, assets, blendId }) => { + return await signTransaction({ + activeUser, + account: 'atomicassets', + action: 'transfer', + data: { + from: activeUser.accountName, + to: 'dappgamemine', + assets_ids: assets, + memo: `blend:${blendId}` + } + }); +}; +``` + +For a successful blend of our tools, let's look at the configurations we need to pass to our function: + +- **owner** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **component_ids** -- this is array of our tool assets for blend; +- **blend_id** -- this is id of blend + +Here is a link to the action in the smart contract for blend tools: + + + +**Upgrading assets** +-------------------- + +The next thing we're going to look at is upgrading our tools and workplaces. + +Our tool starts at level 1, but we can upgrade it. When this tool is staking on the workplace, it calculates the amount of resources needed to upgrade it and, if the user has enough, increases the item's mining_rate. Which, as we know from the previous article, affects how fast our resources are mined. + +![](/public/assets/images/tutorials/howto-create_farming_game/part11/image2.png) + +First, we need to decide which tool or workplace we want to improve. In order to perform this action, our tool needs to be staked. Then we pass it to the configuration of our action. + +The UI should look at the maximum allowed level of the tool we want to upgrade before calling the action. It is written in the data of this tool.  + +The next level should be higher than the current one. For our example, let's assume that our instrument has an initial first level. + +Action for upgrade tool: + +At the beginning of the function, we use a check to see if our tool has reached the maximum level. + +```js +export const upgradeTool = async ({ activeUser, toolID, wpID }) => { + const maxLevel = toolID.data.maxLevel; + const currentLevel = toolID.data.level; + + const nextLvl = currentLevel + 1; + + if (nextLvl > maxLevel) { + throw new Error('Error: The next level exceeds the maximum level of the instrument.'); + } + + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'upgradeitem', + data: { + owner: activeUser.accountName, + item_to_upgrade: toolID.asset_id, + next_level: nextLvl, + staked_at_farmingitem: wpID + } + }); +}; +``` + +Here we use an action game called "upgradeitem". + +- **owner** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **item_to-upgrade** -- ID of the tool we want to upgrade; +- **stake_at_farmingitem** --  the ID of the workplace to which the tool is staking; + +After a successful upgrade, our tool becomes level 2: + +![](/public/assets/images/tutorials/howto-create_farming_game/part11/image3.png) + +Action for workplace tool: + +```js +export const upgradeWorkplace = async ({ activeUser, wpID, stakedBool}) => { + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'upgfarmitem', + data: { + owner: activeUser.accountName, + farmingitem_to_upgrade: wpID, + staked: stakedBool, + } + }); +}; +``` + +Here we use an action game called "upgfarmitem". + +- **owner** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **farmingitem_to_upgrade** -- ID of the workplace we want to upgrade; +- **staked** (boolean) -- If it is staking, then true, if not, then false. + +Here is a link to the action in the smart contract for upgrade tools: + + + +and upgrade workplaces + + + +**Avatar**s +----------- + +Let's take a look at what avatar is and what they're for. + +Avatars are characters that have extra attributes. You can also put items on them that enhance those traits. + +![](/public/assets/images/tutorials/howto-create_farming_game/part11/image4.png) + +This action allows you to set the selected avatar as active and return the old one to our inventory. + +Function for set avatar: + +```js +export const setAvatar = async ({ activeUser, avatarId }) => { + return await signTransaction({ + activeUser, + account: 'atomicassets', + action: 'transfer', + data: { + from: activeUser.accountName, + to: 'dappgamemine', + asset_ids: avatarId, + memo: 'set avatar' + } + }); +}; +``` + +For this action, we use the following configurations: + +- **from** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **to** -- game contract name +- **asset_ids** -- array of avatars +- **memo** -- "set avatar" -- to set your avatar's + +Function for set equipment: + +```js +export const setEquipment = async ({ activeUser, equipments }) => { + return await signTransaction({ + activeUser, + account: 'atomicassets', + action: 'transfer', + data: { + from: activeUser.accountName, + to: 'dappgamemine', + asset_ids: equipments, + memo: 'set equipment' + } + }); +}; +``` + +The configurations are the same as in the previous action, only the memo changes. + +- **asset_ids** -- array of equipments +- **memo** -- "set equipment" -- to set your avatar's equipment + +Here is a link to the action in the smart contract for set avatar and equipment: + + + +We also have avatars table, where for each user there is an array with id that contains their avatar and equipment. For this we will use a read from the table. In the last article we looked at the fetchRows function. + +```js +export const fetchAvatars = async ({ activeUser }) => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: activeUser.accountName, + table: "avatars" + }); + + return rows; + } +``` + +The stats table, which contains the current characteristics of the user (the sum of the equip and avatar stats). + +```js +export const fetchStats = async ({ activeUser }) => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: activeUser.accountName, + table: "stats" + }); + + return rows; +} +``` + +Here is a link to the action in the smart contract for watch avatars: + + + +and stats: + + + +**PS.** The [Following link](https://github.com/dapplicaio/GameAvatars) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part12.md b/docs/build/tutorials/howto-create_farming_game/Part12.md new file mode 100644 index 0000000..6a7b2e7 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part12.md @@ -0,0 +1,127 @@ +Part 12: Token and resource swaps +=== + +In this article, we're building on previous discussions about upgrading items by introducing a method to exchange resources for tokens. We'll add a new table to track resources, where each entry includes a `key_id` (numerical ID for the resource), `resource_name`, and a `ratio` defining how many resources convert to one token. For instance, a ratio of 25 means 100 units of wood would exchange for 4 tokens. We'll also integrate the standard `eosio.token` contract, previously covered, to handle these transactions. + +```cpp + struct [[eosio::table]] resourcecost + { + uint64_t key_id; + std::string resource_name; + float ratio; // if user swap 100 wood and ration is 25 it means that user will receive 4 tokens + + uint64_t primary_key() const { return blend_id; } + }; + typedef multi_index< "resourcecost"_n, resourcecost_j > resourcecost_t; + +``` + +Continuing from the previous setup, we now focus on optimizing the token transaction system. We have pre-minted the entire token supply and assigned it to the game contract, allowing for smooth transfers among players. However, an alternative approach involves dynamically minting tokens as needed, such as when a player exchanges resources for tokens, ensuring just the right amount is created and disbursed. + +Next, we will implement a function to create entries in the `resourcecost` table. This function, restricted to being called only by the contract, will facilitate setting or updating the exchange rates between resources and tokens. The GAME token, integral to our system, features 4 decimal places and is already distributed to the game contract for active gameplay interactions. + +```cpp +void game::setratio(const std::string& resource, const float& ratio) +{ + require_auth(get_self()); + + const uint64_t key_id = stringToUint64(resource); + resourcecost_t resourcecost_table(get_self(), get_self().value); + auto resourcecost_table_itr = resourcecost_table.find(key_id); + + if(resourcecost_table_itr == std::end(resourcecost_table)) + { + resourcecost_table.emplace(get_self(), [&](auto &new_row) + { + new_row.key_id = key_id; + new_row.resource_name = resource; + new_row.ratio = ratio; + }); + } + else + { + resourcecost_table.modify(resourcecost_table_itr, get_self(), [&](auto &new_row) + { + new_row.resource_name = resource; + new_row.ratio = ratio; + }); + } +} + +``` + +Since the initialization code for adding entries to the `resourcecost` table mirrors processes outlined in previous articles, we won't delve into those specifics again here. Now that we've established the resource-to-token relationship, the next step involves writing the swap function. This function will handle the conversion of resources into GAME tokens, facilitating player transactions within the game environment. + +```cpp +void game::swap(const name& owner, const std::string& resource, const float& amount2swap) +{ + require_auth(owner); + + resourcecost_t resourcecost_table(get_self(), get_self().value); + auto resourcecost_table_itr = resourcecost_table.require_find(stringToUint64(resource), "Could not find resource cost config"); + + const float token_amount = amount2swap / resourcecost_table_itr->ratio; + const asset tokens2receive = asset(token_amount * 10000, symbol("GAME", 4)); // change to token you have deployed + + reduce_owner_resources_balance(owner, std::map({{resource, amount2swap}})); + tokens_transfer(owner, tokens2receive); +} + +``` + +where + +**owner** -- player who wants to make a swap + +**resource** -- the name of the resource + +**amount2swap** -- amount of resource that the player wants to exchange for tokens. + +To execute a resource-to-token swap in the game: + +1. **Record Retrieval**: The function first retrieves the relevant record from the resource table based on the specified resource name. If the resource isn't found, an error is thrown. +2. **Token Calculation**: It then calculates the number of tokens the player should receive based on the amount of resource they want to swap and the predefined ratio in the table: *const* *float* token_amount = amount2swap / resourcecost_table_itr->ratio; +3. **Token Asset Creation**: An asset variable, representing the tokens, is created:\ + This step formats the token amount to consider the token's decimal places, ensuring the proper amount is processed for the swap:  *const* **asset** tokens2receive = **asset**(token_amount * 10000, **symbol**("GAME", 4)); + +The multiplication by (10^4) is necessary because the GAME token is defined with 4 decimal places. To accurately reflect the decimal in transactions, the `token_amount` calculated must be scaled up by 10,000. Additionally, the token symbol configuration requires two parameters: the token's name ("GAME") and its number of decimal places (4). This setup ensures that the token amount and its representation are correctly handled in the system for precise and valid transactions. + +Note. While making your own game, make sure to replace GAME with your token name. + +```cpp + reduce_owner_resources_balance(owner, std::map({{resource, amount2swap}})); + tokens_transfer(owner, tokens2receive); +``` + +We are already familiar with this functionality from the previous part and we will also add a token transfer function. + +```cpp +void game::tokens_transfer(const name& to, const asset& quantity) +{ + + action + ( + permission_level{get_self(),"active"_n}, + "tokencontr"_n, // change to your deployed token contract + "transfer"_n, + std::make_tuple + ( + get_self(), + to, + quantity, + std::string("") + ) + ).send(); +} + +``` + +To complete the resource-to-token exchange process, the function initiates a token transfer using the contract's token transfer functionality. You will need to replace the referenced token contract name with the name of your specific token contract to ensure the transfer aligns with your game's tokenomics and smart contract settings. This step finalizes the swap by moving the calculated token amount from the game contract to the player's account. + +"tokencontr"_n + +This article detailed the process of exchanging in-game resources for tokens within a smart contract framework. It covered setting up a table to define resource-to-token conversion rates, calculating the number of tokens based on resources submitted by players, and handling token transactions effectively by ensuring all data aligns with the defined token characteristics. The focus was on the seamless integration of these functionalities into the game's ecosystem, facilitating a dynamic exchange mechanism that enhances player interaction and game economics. + +**PS.** The [Following link](https://github.com/dapplicaio/TokenSwaps) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. + +### \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part13.md b/docs/build/tutorials/howto-create_farming_game/Part13.md new file mode 100644 index 0000000..aea2904 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part13.md @@ -0,0 +1,278 @@ +Part 13: Token Staking and Voting in games +=== + +To delve into token staking and its role in governance, we begin by setting up a new table to manage the staking process. This table will track the staked tokens and their corresponding voting rights, crucial for enabling players to participate in key decision-making processes, such as changing resource-to-token ratios in swaps. This functionality not only deepens player engagement but also decentralizes game governance, empowering players to have a say in the game's economic strategies. + +```cpp +struct [[eosio::table]] balance_j + { + name owner; + asset quantity; + + uint64_t primary_key() const { return owner.value; } + }; +typedef multi_index< "balance"_n, balance_j > balance_t; +``` + +For the implementation of token staking, we'll establish a new table with fields for 'owner' and 'number of staked tokens'. This table will track the tokens each player has staked in the game. Additionally, to facilitate staking, a function will be added to listen for token transfers. This function will automatically update the staking table whenever tokens are transferred to the contract, ensuring that players' staked tokens are accurately recorded and managed. + +```cpp +[[eosio::on_notify("tokencont::transfer")]] // tokencont change for your token contract +void receive_token_transfer +( + const name& from, + const name& to, + const asset& quantity, + const std::string& memo +); +``` + +When setting up the token staking functionality, make sure to customize the code by replacing `tokencont` with the actual name of your token contract. This is essential for ensuring that the staking function correctly interacts with the specific token contract deployed for your game, allowing for accurate tracking and management of staked tokens. + +```cpp +void game::receive_token_transfer +( + const name& from, + const name& to, + const asset& quantity, + const std::string& memo +) +{ + if(to != get_self()) + return; + + if(memo == "stake") + { + increase_tokens_balance(from, quantity); + } + else + check(0, "Invalid memo"); +} + +``` + +and + +```cpp +void game::increase_tokens_balance(const name& owner, const asset& quantity) +{ + balance_t balance_table(get_self(), get_self().value); + auto balance_table_itr = balance_table.find(owner.value); + + if(balance_table_itr == std::end(balance_table)) + { + balance_table.emplace(get_self(), [&](auto &new_row) + { + new_row.owner = owner; + new_row.quantity = quantity; + }); + } + else + { + balance_table.modify(balance_table_itr, get_self(), [&](auto &new_row) + { + new_row.quantity += quantity; + }); + } +} + +``` + +The function for managing token staking operates by accessing the balance table to locate a specific player's entry. If the player already has a recorded balance, the function increments the number of staked tokens accordingly. If no existing balance is found, it creates a new record for the player, documenting the amount of tokens they have staked. + +Voting +------ + +Now that token staking is in place, we'll focus on implementing a voting system to change the rate in resource swaps. To facilitate this, we'll introduce a new table specifically designed to manage voting records. This table will track each vote related to rate adjustments, allowing staked token holders to influence the resource-to-token conversion rates based on their preferences and stake in the game. This mechanism integrates democratic decision-making into the game's economic model. + +```cpp + struct [[eosio::table]] changeration_j + { + uint64_t voting_id; + std::string resource_name; + float new_ratio; + std::map voted; // first is player name, second is voting power (in tokens) + + uint64_t primary_key() const { return voting_id; } + }; + typedef multi_index< "changeration"_n, changeration_j > changeration_t; + +``` + +To support voting on changes in resource swap rates, we will establish a new table structured as follows: + +- **voting_id**: A unique identifier for each voting event. +- **resource_name**: The name of the resource subject to the rate change. +- **new_ratio**: The proposed new exchange ratio of the resource to the token. +- **voted**: A list detailing which players have voted, along with the number of votes each player has cast, reflecting their staked token amounts. + +This setup allows token holders to participate directly in decisions affecting the game's economic dynamics. + +```cpp +void game::createvoting( + const name& player, + const std::string& resource_name, + const float& new_ratio +) +{ + require_auth(player); + + const uint64_t key_id = stringToUint64(resource_name); + resourcecost_t resourcecost_table(get_self(), get_self().value); + auto resourcecost_table_itr = resourcecost_table.require_find(key_id, "Could not find selected resource name"); + + changeration_t changeration_table(get_self(), get_self().value); + const uint64_t new_voting_id = changeration_table.available_primary_key(); + + changeration_table.emplace(player, [&](auto &new_row) + { + new_row.voting_id = new_voting_id; + new_row.resource_name = resource_name; + new_row.new_ratio = new_ratio; + }); +} + +``` + +To begin with, we check whether such a resource exists in the config table: + +```cpp + const uint64_t key_id = stringToUint64(resource_name); + resourcecost_t resourcecost_table(get_self(), get_self().value); + auto resourcecost_table_itr = resourcecost_table.require_find(key_id, "Could not find selected resource name"); + +``` + +after that, we extract a new id for voting + +```cpp + changeration_t changeration_table(get_self(), get_self().value); + const uint64_t new_voting_id = changeration_table.available_primary_key(); + +``` + +and make a new record, note that the player now pays for the frames to avoid abuse + +```cpp + changeration_table.emplace(player, [&](auto &new_row) + { + new_row.voting_id = new_voting_id; + new_row.resource_name = resource_name; + new_row.new_ratio = new_ratio; + }); + +``` + +Now let's create a function for voting. Let's imagine that conditionally we need 100 votes (100 tokens for the vote to be completed and the changes to be approved). + +```cpp +void game::vote( + const name& player, + const uint64_t& voting_id +) +{ + require_auth(player); + + balance_t balance_table(get_self(), get_self().value); + resourcecost_t resourcecost_table(get_self(), get_self().value); + changeration_t changeration_table(get_self(), get_self().value); + + auto balance_table_itr = balance_table.require_find(player.value, "You don't have staked tokens to vote"); + auto changeration_table_itr = changeration_table.require_find(voting_id, "Could not find selected voting id"); + auto resourcecost_table_itr = resourcecost_table.require_find(stringToUint64(changeration_table_itr->resource_name)); + + const asset goal_votes = asset(100 * 10000, symbol("GAME", 4)); // 100.0000 GAME tokens to apply changes + asset total_votes = asset(0, symbol("GAME", 4)); + + for (const auto& map_itr : changeration_table_itr->voted) + total_votes += map_itr.second; + + if(total_votes + balance_table_itr->quantity >= goal_votes) + { + resourcecost_table.modify(resourcecost_table_itr, get_self(), [&](auto &new_row) + { + new_row.ratio = changeration_table_itr->new_ratio; + }); + + changeration_table.erase(changeration_table_itr); + } + else + { + changeration_table.modify(changeration_table_itr, get_self(), [&](auto &new_row) + { + new_row.voted[player] = balance_table_itr->quantity; + }); + } +} + +``` + +Let's describe the code above in parts: + +1. player authorization + +```cpp + require_auth(player); +``` + +2\. For effective management and processing of player votes regarding resource-to-token ratio changes, the system incorporates critical data structures. These include the Token Balance Table to check players' staked tokens for voting power, the Resource Price Config to reference current and proposed ratio changes, and the Voting Table to accurately manage and tally votes using its iterators. These components are essential for ensuring transparency and integrity in the game's democratic decision-making process. + +```cpp +balance_t balance_table(get_self(), get_self().value); +resourcecost_t resourcecost_table(get_self(), get_self().value); +changeration_t changeration_table(get_self(), get_self().value); + +auto balance_table_itr = balance_table.require_find(player.value, "You don't have staked tokens to vote"); +auto changeration_table_itr = changeration_table.require_find(voting_id, "Could not find selected voting id"); +auto resourcecost_table_itr = resourcecost_table.require_find(stringToUint64(changeration_table_itr->resource_name)); +``` + +3\. For the voting process, a specific variable is initialized to track progress towards the approval threshold predefined in the vote setup. If this threshold is met, the vote is considered successful, and changes can be applied to the resource ratio settings. + +```cpp +const asset goal_votes = asset(100 * 10000, symbol("GAME", 4)); // 100.0000 GAME tokens to apply changes + asset total_votes = asset(0, symbol("GAME", 4)); + +``` + +4\. So total votes count needs to be done + +```cpp +for (const auto& map_itr : changeration_table_itr->voted) + total_votes += map_itr.second; + +``` + +5\. If the total votes reach the threshold set for the proposal, indicating approval by the players, then the resource price configuration they voted on is updated accordingly. Following this update, the specific vote is concluded and removed from the voting table, finalizing the decision and reflecting the players' collective choice in the game's settings. + +```cpp +if(total_votes + balance_table_itr->quantity >= goal_votes) +{ + resourcecost_table.modify(resourcecost_table_itr, get_self(), [&](auto &new_row) + { + new_row.ratio = changeration_table_itr->new_ratio; + }); + + changeration_table.erase(changeration_table_itr); +} + +``` + +6\. otherwise, simply add the votes currently cast by the player + +```cpp +else +{ + changeration_table.modify(changeration_table_itr, get_self(), [&](auto &new_row) + { + new_row.voted[player] = balance_table_itr->quantity; + }); +} + +``` + +This article focused on implementing a token staking and voting system within a game environment. It detailed setting up a voting structure for players to influence changes in resource-to-token exchange rates through a democratic process. Key components included creating tables for tracking votes and configuring tokens staked by players to determine their voting power. The article also described how votes are tallied and the conditions under which proposed changes are implemented, emphasizing the integration of these functionalities into the game's smart contract framework. + +**PS.** The [Following link](https://github.com/dapplicaio/TokenStakingAndVoting) leads us to a repository that corresponds everything described. + +### \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part14.md b/docs/build/tutorials/howto-create_farming_game/Part14.md new file mode 100644 index 0000000..c6c431b --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part14.md @@ -0,0 +1,538 @@ +Part 14: Governance in games +=== + +In this article, we will develop a voting system that will allow users to provide developers with suggestions or change the value of game changes. + +1. Creating configs table + +```cpp +struct [[eosio::table]] mconfig_j + { + std::string variable_name; + std::string variable_type; + std::string variable_value; + + uint64_t primary_key() const { return stringToUint64(variable_name); } + }; + typedef multi_index<"config"_n, mconfig_j> mconfigs_t; + +``` + +The table is organized from rows (variable name, variable type, variable value) + +```cpp +typedef std::variant CONFIG_TYPE; + +void game::setcnfg( + const std::string &variable_name, + const CONFIG_TYPE &variable) +{ + require_auth(get_self()); + mconfigs_t configs_table(get_self(), get_self().value); + + auto var_iter = configs_table.find(stringToUint64(variable_name)); + + auto variant_data = get_config_variant_data(variable); + + if (var_iter == configs_table.end()) + { + configs_table.emplace(get_self(), [&](auto &new_row) + { + new_row.variable_name = variable_name; + new_row.variable_type = variant_data.first; + new_row.variable_value = variant_data.second; }); + } + else + { + check(var_iter->variable_type == variant_data.first, "Types mismatch"); + configs_table.modify(var_iter, get_self(), [&](auto &row) + { row.variable_value = variant_data.second; }); + } +} + +``` + +The **CONFIG_TYPE** type indicates the valid types for configuration variables. The **setcnfg** action accepts the variable name and its value (along with the type, the **eosio** platform itself checks for the correct type). If this variable is present in the config table, we check whether the type of the variable matches the one passed by the user and change the table field. If the variable is not in the table, we add it. + +```cpp +std::pair game::get_config_variant_data(const CONFIG_TYPE &var) +{ + if (std::holds_alternative(var)) + { + return {"string", std::get(var)}; + } + else if (std::holds_alternative(var)) + { + return {"float", std::to_string(std::get(var))}; + } + else if (std::holds_alternative(var)) + { + return {"uint32", std::to_string(std::get(var))}; + } + else if (std::holds_alternative(var)) + { + return {"int32", std::to_string(std::get(var))}; + } + else + { + return {"error", ""}; + } +} + +``` + +This helper function translates **CONFIG_TYPE** data into a pair (variable type, variable value). + +2\. Main tables + +```cpp +struct [[eosio::table]] votings_info_j + { + name voting_name; + name creator; + uint64_t min_staked_tokens; + uint64_t max_staked_tokens; + uint64_t total_staked; + uint32_t creation_time; + uint32_t time_to_vote; + std::string variable_to_change; + + uint64_t primary_key() const { return voting_name.value; }; + }; + typedef multi_index<"vtsinfo"_n, votings_info_j> vinfo_t; +``` + +This table records voting information in a separate table. With its help, we can view the total number of staked tokens, see if the voting time has expired, and so on. + +```cpp + struct [[eosio::table]] voting_j + { + uint64_t id; + std::string voting_option; + std::map voted; + uint64_t total_voted_option; + + uint64_t primary_key() const { return id; } + }; + typedef multi_index<"genvtngs"_n, voting_j> votings_t; + +``` + +This table (scope -- the name of the vote) stores the voting options and std::map with pairs (player, number of staked tokens), as well as the total amount of tokens staked for this option. + +```cpp + struct [[eosio::table]] closed_votings_j + { + uint64_t voting_id; + name voting_name; + std::string winner_option; + + uint64_t primary_key() const { return voting_id; }; + }; + typedef multi_index<"closedvts"_n, closed_votings_j> closed_votings_t; +``` + +This table stores pairs (voting, voting result). + +3\. Creating votings + +```cpp +void game::crgenvt( + const name &player, + const name &voting_name, + const std::vector &options, + uint64_t min_staked_tokens, + uint32_t time_to_vote, + uint64_t max_staked_tokens) + +{ + require_auth(player); + vinfo_t vinfo(get_self(), get_self().value); + check(vinfo.find(voting_name.value) == std::end(vinfo), "Voting with this name is active"); + + // add to info + + vinfo.emplace(get_self(), [&](auto &new_row) + { + new_row.voting_name = voting_name; + new_row.creator = player; + new_row.min_staked_tokens = min_staked_tokens; + new_row.max_staked_tokens = max_staked_tokens; + new_row.total_staked = 0; + new_row.creation_time = current_time_point().sec_since_epoch(); + new_row.time_to_vote = time_to_vote; }); + + add_voting_with_options(voting_name, options); +} +``` + +This function writes voting data to the **vtsinfo** table and adds fields to the **genvtngs** table (option name, map with voters, total amount of tokens staked for the option). + +```cpp +void game::cravote( + const name &player, + const name &voting_name, + const std::string &variable_name, + const std::vector &options_typed, + int64_t min_staked_tokens, + int32_t time_to_vote, + uint64_t max_staked_tokens) +{ + require_auth(player); + vinfo_t vinfo(get_self(), get_self().value); + check(vinfo.find(voting_name.value) == std::end(vinfo), "Voting with this name is active"); + + // check type correctness + + std::vector options; + mconfigs_t configs_table(get_self(), get_self().value); + auto config_iter = configs_table.require_find(stringToUint64(variable_name), "No such variable in configs table"); + std::string variable_type = config_iter->variable_type; + + for (const auto &option : options_typed) + { + auto variant_data = get_config_variant_data(option); + check(variant_data.first == variable_type, "Types mismatch in options"); + options.push_back(variant_data.second); + } + + // add to info + + vinfo.emplace(get_self(), [&](auto &new_row) + { + new_row.voting_name = voting_name; + new_row.creator = player; + new_row.min_staked_tokens = min_staked_tokens; + new_row.max_staked_tokens = max_staked_tokens; + new_row.total_staked = 0; + new_row.creation_time = current_time_point().sec_since_epoch(); + new_row.time_to_vote = time_to_vote; + new_row.variable_to_change = variable_name; }); + + add_voting_with_options(voting_name, options); +} +``` + +This function creates a vote that affects some variable from the config table. First, for each option, we check if its type matches the variable name. If so, we add information about the vote to the **vtsinfo** and **genvtngs** tables. + +```cpp +void game::add_voting_with_options(const name &voting_name, const std::vector &options) +{ + votings_t votings(get_self(), voting_name.value); + + for (uint64_t index = 0; index < options.size(); index++) + { + votings.emplace(get_self(), [&](auto &new_row) + { + new_row.id = index; + new_row.voting_option = options[index]; + new_row.voted = {}; + new_row.total_voted_option = 0; }); + } +} + +``` + +This is an auxiliary function that adds rows to the **genvtngs** table (id, option name, map with voters, total number of tokens). + +**4\.** Voting process + +```cpp +void game::gvote( + const name &player, + const name &voting_name, + const std::string &voting_option, + uint64_t voting_power) +{ + require_auth(player); + balance_t balance_table(get_self(), get_self().value); + auto player_balance_iter = balance_table.require_find(player.value, "No tokens staked"); + check(player_balance_iter->quantity.amount >= voting_power, "Not enough tokens to vote with that voting power"); + + vinfo_t vinfo(get_self(), get_self().value); + auto voting_info_iter = vinfo.require_find(voting_name.value, "No such voting"); + + // check time + + if (voting_info_iter->time_to_vote != 0) + { + check((current_time_point().sec_since_epoch() - voting_info_iter->creation_time) <= voting_info_iter->time_to_vote, + "Time for voting is over"); + } + + // check max limit + + if (voting_info_iter->max_staked_tokens != 0) + { + check(voting_info_iter->total_staked < voting_info_iter->max_staked_tokens, "Max limit for staking is reached"); + } + + votings_t votings(get_self(), voting_name.value); + auto option_iter = votings.end(); + auto current_iter = votings.end(); + uint64_t id = 0; + + while (true) + { + current_iter = votings.find(id); + + if (current_iter == votings.end()) + { + break; + } + + if (current_iter->voting_option == voting_option) + { + option_iter = current_iter; + } + + check(current_iter->voted.find(player) == current_iter->voted.end(), "Already voted"); + + id++; + } + + check(option_iter != votings.end(), "No such option"); + + votings.modify(option_iter, get_self(), [&](auto &row) + { + + row.voted[player] = voting_power; + row.total_voted_option += voting_power; }); + + vinfo.modify(voting_info_iter, get_self(), [&](auto &row) + { row.total_staked += voting_power; }); + + balance_table.modify(player_balance_iter, get_self(), [&](auto& row) + { + row.quantity.amount -= voting_power; + }); + + // check max limit and close if it's achieved + if (voting_info_iter->max_staked_tokens != 0) + { + if (voting_info_iter->total_staked >= voting_info_iter->max_staked_tokens) + { + if (voting_info_iter->variable_to_change.empty()) + { + close_general_voting(voting_name); + } + else + { + close_automatic_voting(voting_name); + } + } + } +} +``` + +First, this function checks whether the user has tokens, whether they are sufficient in number, and whether the vote to which they are referring exists. Then we check whether the voting time has expired and whether the upper limit of votes has been reached. If so, it is no longer possible to vote.  + +Otherwise, we go through the voting options and check whether the one the player is referring to is among them. We also check if they have voted before. If they have, we interrupt the function.  + +If the option is found, we modify the field of the **genvtngs** table -- we add the player to std::map, increase the total number of tokens staked for the option. In the **vtsinfo** table, increase the **total_tokens_staked** field. We also charge the player the specified number of tokens. + +At the very end, check whether the maximum number of votes has been reached. If so, close the voting (more details are provided later in the article). + +```cpp +void game::cancelvote(const name& player, const name& voting_name) +{ + require_auth(player); + + vinfo_t vinfo(get_self(), get_self().value); + auto voting_info_iter = vinfo.require_find(voting_name.value, "No such voting"); + + balance_t balance_table(get_self(), get_self().value); + auto player_balance_iter = balance_table.require_find(player.value, "No staked tokens for this player"); + + votings_t votings(get_self(), voting_name.value); + auto option_iter = votings.end(); + auto current_iter = votings.end(); + + uint64_t voting_power = 0; + uint32_t id = 0; + + while (true) + { + current_iter = votings.find(id); + + if (current_iter == votings.end()) + { + break; + } + + auto player_voting_iter = current_iter->voted.find(player); + if (player_voting_iter != current_iter->voted.end()) + { + voting_power = player_voting_iter->second; + + votings.modify(current_iter, get_self(), [&](auto& row) + { + row.voted.erase(player_voting_iter); + row.total_voted_option -= voting_power; + }); + + break; + } + + id++; + } + + if (voting_power != 0) + { + vinfo.modify(voting_info_iter, get_self(), [&](auto& row) + { + row.total_staked -= voting_power; + }); + + balance_table.modify(player_balance_iter, get_self(), [&](auto& row) + { + row.quantity.amount += voting_power; + }); + } +} +``` + +This feature allows a player to cancel their vote in a poll. Let's go through the voting options. If we find the one that the player voted for, we remove it from the map, reduce the number of staked tokens in both tables. We return the player's tokens back to him. + +5\. Closing voting + +```cpp +void game::clsvt(const name &voting_name) +{ + vinfo_t vinfo(get_self(), get_self().value); + auto voting_info_iter = vinfo.require_find(voting_name.value, "No such voting"); + + require_auth(voting_info_iter->creator); + + if (voting_info_iter->variable_to_change.empty()) + { + close_general_voting(voting_name); + } + else + { + close_automatic_voting(voting_name); + } +} +``` + +We should check if such a vote exists. Then we call separate functions for two different types of voting. + +```cpp +void game::close_general_voting(const name &voting_name) +{ + clear_voting_from_vinfo(voting_name); + std::string winner = get_voting_winner_clear(voting_name); + + // add to closed + closed_votings_t closed_votings(get_self(), get_self().value); + uint64_t voting_id = closed_votings.available_primary_key(); + + closed_votings.emplace(get_self(), [&](auto &new_row) + { + new_row.voting_id = voting_id; + new_row.voting_name = voting_name; + new_row.winner_option = winner; }); +} + +``` + +First, delete the voting data from the **vtsinfo** table. Then we get the option with the largest number of staked tokens and delete the fields from the **genvtngs** table. Since the voting is of the general type, we add a row (id, voting name, winning option) to the table with the results of the general voting. + +```cpp +void game::close_automatic_voting(const name &voting_name) +{ + std::string variable_name = clear_voting_from_vinfo(voting_name); + std::string winner = get_voting_winner_clear(voting_name); + + // do transaction + mconfigs_t config_table(get_self(), get_self().value); + auto config_iter = config_table.require_find(stringToUint64(variable_name), "Variable in question was deleted"); + config_table.modify(config_iter, get_self(), [&](auto &row) + { row.variable_value = winner; }); +} +``` + +Closing automatic voting differs in that we modify the value of the variable in the config table at the end. + +```cpp +std::string game::clear_voting_from_vinfo(const name &voting_name) +{ + vinfo_t vinfo(get_self(), get_self().value); + auto voting_info_iter = vinfo.require_find(voting_name.value, "No such voting"); + + std::string variable_name = voting_info_iter->variable_to_change; + uint64_t min_staked = voting_info_iter->min_staked_tokens; + uint64_t total_staked = voting_info_iter->total_staked; + + check(total_staked >= min_staked, "Minimal rate is not reached yet"); + + if (voting_info_iter->time_to_vote != 0) + { + check((current_time_point().sec_since_epoch() - voting_info_iter->creation_time) >= voting_info_iter->time_to_vote, + "Time for voting is not over yet"); + } + + if (voting_info_iter->max_staked_tokens != 0) + { + check(total_staked >= voting_info_iter->max_staked_tokens, "Voting limit is not reached yet"); + } + + vinfo.erase(voting_info_iter); + + return variable_name; +} +``` + +This function checks whether the conditions for closing the vote are met and deletes the data about it from the **vtsinfo** table. + +```cpp +std::string game::get_voting_winner_clear(const name &voting_name) +{ + std::string winner = ""; + uint64_t max_voted = 0; + uint64_t id = 0; + + votings_t votings(get_self(), voting_name.value); + + balance_t balance_table(get_self(), get_self().value); + + while (true) + { + auto option_iter = votings.find(id); + + if (option_iter == votings.end()) + { + break; + } + + if (option_iter->total_voted_option > max_voted) + { + max_voted = option_iter->total_voted_option; + winner = option_iter->voting_option; + } + + for (const auto& player_amount : option_iter->voted) + { + auto player_balance_iter = balance_table.require_find(player_amount.first.value, "Player was deleted"); + balance_table.modify(player_balance_iter, get_self(), [&](auto& row) + { + row.quantity.amount += player_amount.second; + }); + } + + votings.erase(option_iter); + + id++; + } + + return winner; +} +``` + +This function goes through the voting options, finds the winning option, and deletes the rows of the genvtngs table one by one. It also returns the player's tokens to the balance. + +In this article we described a step by step process of creating governance via voting, so users can impact overall project game design or certain parts of game economy. + +**PS.** The [Following link](https://github.com/dapplicaio/GamesGovernane) leads us to a repository that corresponds everything described. + +### \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part15.md b/docs/build/tutorials/howto-create_farming_game/Part15.md new file mode 100644 index 0000000..c177089 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part15.md @@ -0,0 +1,218 @@ +Part 15: GUI for swaps, staking and governance +=== + +In addition to the previous articles integrating it into a ReactJS interface, and reading data from a WAX smart contract table, we will also dive deeper into swaps. We will look at the use of WAX tokens or NFTs in games and how user/player governance works. + +**Token swaps** +--------------- + +The swap is designed to exchange the amount of a resource at the rate specified in the table called "**resourcecost**". After the action is completed, the contract sends WAX tokens (they are not staked, the player can stake them later). + +In simple terms, you sell your resource and receive WAX tokens for it. To do this, you need to call the contract action "**swap**". + +```js +export const swap = async ({ activeUser, resource, amount2swap }) => { + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'swap', + data: { + owner: activeUser.accountName, + resource: resource, + amount2swap: amount2swap + } + }); +}; +``` + +Here we use an action game called "swap". + +- **owner** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **resource** -- name of resource; +- **amount2swap** (number) -- amount for swap. + +Now let's look at the "**resourcecost**" table. + +```js +export const resourceСost = async () => { +const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: 'dappgamemine', + table: "resourcecost" +}); + return rows; +} + +``` + +The table is structured as follows: each row is identified by an 'id', which is a unique identifier. Next to it is the 'resource name', which is the type or name of the resource. In addition, the 'ratio' column indicates the ratio of the resource to the WAX token. This 'ratio' essentially quantifies the relationship between the resource and its associated WAX token, providing a measure of their interchangeability or equivalence. + +This is how it looks on the UI: + +![](/public/assets/images/tutorials/howto-create_farming_game/part15/image1.png) + +The user selects the resource in the random mill and enters the quantity. After that, the amount of WAX will automatically change in the lower field. + +Here is a link to the action in the smart contract for swap: + + + +and table **resourcecost:** + + + +**Token Staking in games** +-------------------------- + +Got it! Let's delve into the process of staking WAX tokens and using them for voting. We'll explore how to call the necessary action to stake our WAX tokens and gain the right to vote. + +```js +export const tokenStake = async ({ activeUser, quantity }) => { + return await signTransaction({ + activeUser, + account: 'eosio.token', + action: 'transfer', + data: { + from: activeUser.accountName, + to: 'dappgamemine', + quantity: quantity, + memo: 'staking' + } + }); +}; + +``` + +Here we use an action called "transfer" for stake WAX tokens. + +- **from** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **to** -- name of game contract; +- **quantity** (string, like '10.00000000 WAX') -- amount for stake. +- **memo --** if we want to stake our WAX in our game, we use MEMO 'staking'; + +And here's how we can get the data from the balance table mentioned above. + +```js +export const fetchBalance = async () => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: 'dappgamemine', + table: "balance" + }); + + return rows; +} + +``` + +On the UI it looks like this: the user enters the amount of WAX he wants to bet, and after clicking on the "stake" button, the action described above is called. + +![](/public/assets/images/tutorials/howto-create_farming_game/part15/image2.png) + +Here is a link to the action in the smart contract for stake WAX tokens: + + + +and balance table: + + + +Game **governance by users/players** +------------------------------------ + +Let's outline the structure of the voting system: + +- Voting can originate from developers (system voting) or from players with sufficient staked tokens (community voting). +- Voting can be related to contract variables (automatic) and can be a proposal that the team commits to fulfil (general). +- A voting is considered valid if a sufficient part of the game community has participated in it (lower bound). + +Here are the conditions for closing a vote: + +- The number of tokens locked in the vote reaches a predetermined limit (closing limit). +- The specified time frame for the vote elapses (deadline). + +When creating a poll, you choose the type of its completion. Either 1 or 2, or hybrid (1 or 2). + +The voting power of a player directly depends on the number of WAX tokens staked. + +Now let's see how to integrate this into React JS. In order to create a poll, we have a function with the name **createVoting()**. + +```js +export const createVoting = async ({ activeUser, resName, ratio }) => { + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'createvoting', + data: { + player: activeUser.accountName, + resource_name: resName, + new_ratio: ratio, + } + }); +}; + +``` + +Here we use an action called "**createvoting**" to create a vote to change the ratio. + +- **player** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **resource_name (string)** -- name of resource; +- **new_ratio** (float32) -- new ratio. + +In the UI image, we see a vote to change the ratio. + +![](/public/assets/images/tutorials/howto-create_farming_game/part15/image3.png) + +It also lists the purpose of the poll, the deadline, the author, the number of participants, and the total number of tokens contributed. + +Now let's see how to vote. First, we need to secure WAX tokens. We have described how to do this above. + +```js +export const vote = async ({ activeUser, id }) => { + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'vote', + data: { + player: activeUser.accountName, + voting_id: id, + } + }); +}; + +``` + +Here we use an action called "vote" to cast your vote. + +- **player** -- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **voting_id** -- voting ID from the сhangeration table; + +Once created, your vote is added to the table '**changeration**' . To extract it for our UI, we use the **changeRation()** function. + +```js +export const changeRation = async () => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: 'dappgamemine', + table: "changeration" + }); + return rows; + } + +``` + +Here is a link to the action in the smart contract for create voting: + + + +action for vote: + + + +a table with all created rate change votes: + + + +**PS.** The [Following link](https://github.com/dapplicaio/GUIStakingGovernanceSwaps) leads us to a repository that corresponds everything described. + +### \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part16.md b/docs/build/tutorials/howto-create_farming_game/Part16.md new file mode 100644 index 0000000..eb20ffe --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part16.md @@ -0,0 +1,200 @@ +Part 16: Leaderboards in games +=== + +In this article, we will analyze the creation of leaderboards and consider the implementation of a leaderboard for user resources and their overall mining rate. + +1. **New tables** + +```cpp + struct [[eosio::table]] lboard_j + { + name account; + uint64_t points; + + uint64_t primary_key() const { return account.value; }; + }; + typedef multi_index<"lboards"_n, lboard_j> lboards_t; + +``` + +points -- the value of points in the leaderboard + +account -- the name of the account for which we record points + +**2\. Updating points in leaderboard** + +To modify the values of the points in the leaderboard, we will introduce three functions: increase, decrease, and set a certain number of points. Let's consider the last one, the others are done in the same way. + +```cpp +void incr_lb_points(const name &lbname, const name &account, uint64_t points); +void decr_lb_points(const name &lbname, const name &account, uint64_t points); +void set_lb_points(const name &lbname, const name &account, uint64_t points); +``` + +```cpp +void game::set_lb_points(const name &lbname, const name &account, uint64_t points) +{ + lboards_t lboard(get_self(), lbname.value); + + auto account_itr = lboard.find(account.value); + + if (account_itr == std::end(lboard)) + { + lboard.emplace(get_self(), [&](auto &new_row) + { + new_row.account = account; + new_row.points = points; }); + } + else + { + lboard.modify(account_itr, get_self(), [&](auto &row) + { row.points = points; }); + } +} + +``` + +Function description: + +1) Take the pointer to the leaderboard table by the name of the leaderboard. Then we found the record with the player + +```cpp +lboards_t lboard(get_self(), lbname.value); +auto account_itr = lboard.find(account.value); +``` + +2) If the player is not in the table, add him or her to the table. If there is, update the points value + +```cpp +if (account_itr == std::end(lboard)) +{ + lboard.emplace(get_self(), [&](auto &new_row) + { + new_row.account = account; + new_row.points = points; }); +} +else +{ + lboard.modify(account_itr, get_self(), [&](auto &row) + { row.points = points; }); +} +``` + +**3\. Using leaderboards for resource records** + +In the **increase_owner_resource_balance** and **decrease_owner_resource_balance** functions, add the following lines inside the loop that runs through the map with resources + +```cpp +eosio::name resource_name(static_cast(resources_table_itr->resource_name)); +set_lb_points(resource_name, owner, resources_table_itr->amount); + +``` + +The first of them creates **eosio::name** from a string denoting the name of the resource. That is, it is a leaderboard of wood or stone, etc. Then we set the already calculated value of the resource in the table. + +**4\. Using leaderboards for mining rate** + +At the very end of the stake_items and **upgradeitem** functions, add a line with the **upgrade_mining_power_lb** function, which recalculates the new total mining rate and enters it into the leaderboard. + +```cpp +void game::update_mining_power_lb(const name &account) +{ + staked_t staked_table(get_self(), account.value); + float mining_power = 0.0f; + + const std::map stats = get_stats(account); + auto assets = atomicassets::get_assets(get_self()); + + for (const auto &staked : staked_table) + { + auto farmingitem_itr = assets.find(staked.asset_id); + auto farmingitem_mdata = get_mdata(farmingitem_itr); + + float miningBoost = 1; + if (farmingitem_mdata.find("miningBoost") != std::end(farmingitem_mdata)) + miningBoost = std::get(farmingitem_mdata["miningBoost"]); + + for (const uint64_t asset_id : staked.staked_items) + { + mining_power += get_mining_power(asset_id, stats); + } + } + + set_lb_points("miningpwr"_n, account, mining_power); +} + +``` + +Function description: + +1. Take the pointer to the table where all the stacked items are located. We get a map with the player's characteristics. Take the pointer to the table of assets. + +```cpp +staked_t staked_table(get_self(), account.value); +float mining_power = 0.0f; + +const std::map stats = get_stats(account); +auto assets = atomicassets::get_assets(get_self()); +``` + +2\. Go through all the staked items. For each one, we take a pointer to its asset and find the value of miningBoost. Then, for each asset inside the staked one, we calculate the mining rate and add it to the total amount + +```cpp +for (const auto &staked : staked_table) + { + auto farmingitem_itr = assets.find(staked.asset_id); + auto farmingitem_mdata = get_mdata(farmingitem_itr); + + float miningBoost = 1; + if (farmingitem_mdata.find("miningBoost") != std::end(farmingitem_mdata)) + miningBoost = std::get(farmingitem_mdata["miningBoost"]); + + for (const uint64_t asset_id : staked.staked_items) + { + mining_power += get_mining_power(asset_id, stats); + } + } + +``` + +3\. Update the mining rate for this player in the leaderboard + +```cpp +set_lb_points("miningpwr"_n, account, mining_power); +``` + +Consider the function used to calculate the mining rate of an item + +```cpp +float game::get_mining_power(const uint64_t asset_id, const std::map &stats) +{ + auto assets = atomicassets::get_assets(get_self()); + auto assets_itr = assets.require_find(asset_id, "asset not found"); + + auto item_mdata = get_mdata(assets_itr); + auto item_template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); + + const float &miningRate = std::get(item_template_idata["miningRate"]); + + uint8_t current_lvl = 1; + + if (item_mdata.find("level") != std::end(item_mdata)) + { + current_lvl = std::get(item_mdata["level"]); + } + + const uint8_t upgrade_percentage = 2 + stats.at("vitality") / 10.0f; + + float miningRate_according2lvl = miningRate + stats.at("productivity") / 10.0f; + for (uint8_t i = 1; i < current_lvl; ++i) + miningRate_according2lvl = miningRate_according2lvl + (miningRate_according2lvl * upgrade_percentage / 100); + + return miningRate_according2lvl; +} +``` + +It completely repeats the logic of calculating the mining rate asset, which was described in previous articles and was used in claim and **upgradeitem**. + +In this article, we described on how we can create various leaderboards in the game, which is one of core function + +**PS.** The [Following link](https://github.com/dapplicaio/GameLeaderboards) leads us to a repository that corresponds everything described. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part17.md b/docs/build/tutorials/howto-create_farming_game/Part17.md new file mode 100644 index 0000000..2281ff7 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part17.md @@ -0,0 +1,162 @@ +Part 17: Quest systems in game +=== + +In this article, we will add a quest system that will allow us to engage more with the players in our games. + +1. **Let's add the necessary tables** + +```cpp +struct quest +{ + std::string type; + float required_amount; + float current_amount; +}; + +struct [[eosio::table]] quests_j +{ + name player; + std::vector quests; + + uint64_t primary_key() const {return player.value;}; +}; +typedef multi_index<"quests"_n, quests_j> quests_t; + +``` + +The quest structure denotes a triple (quest type, required number of actions/resources/tokens, current number of actions/resources/tokens), where the quest type indicates what the player needs to do. Examples of types are below. + +The quests table consists of the following strings (player, quests[]) + +**2\. Adding and deleting quests** + +```cpp +void game::addquest( + const name &player, + const std::string &type, + float required_amount) +{ + quests_t quests_table(get_self(), get_self().value); + auto player_iter = quests_table.find(player.value); + + quest temp_quest = {type, required_amount, 0.0f}; + + if (player_iter == quests_table.end()) + { + quests_table.emplace(get_self(), [&](auto& new_row) + { + new_row.player = player; + new_row.quests = {temp_quest}; + }); + } + else + { + quests_table.modify(player_iter, get_self(), [&](auto& row) + { + row.quests.push_back(temp_quest); + }); + } +} +``` + +Here we take the pointer to the table. If there is a player, we add a new quest to the already created vector. If there is no player, we create a row for him or her and add the quest to the vector. + +```cpp +void game::cmpltquest( + const name &player, + uint32_t quest_index) +{ + quests_t quests_table(get_self(), get_self().value); + auto player_iter = quests_table.require_find(player.value, "No such quest"); + + check(player_iter->quests.size() > quest_index, "Index outside of scope"); + + quest temp = player_iter->quests[quest_index]; + check(temp.current_amount >= temp.required_amount, "Quest conditions are not met"); + + quests_table.modify(player_iter, get_self(), [&](auto& row){ + row.quests.erase(row.quests.begin() + quest_index); + }); +} +``` + +Take the iterator to the table. Check if there is a field for this player. Then we check if there is a quest for this index in the vector. Check if the quest requirements are met. If so, delete this quest from the array. + +**3\. Connecting the quest system with other game systems** + +Let's introduce several types of quests for example + +**"Staking" -- to stake X tools** + +```cpp +update_quests(owner, "staking", items_to_stake.size()); +``` + +Added a line in the stake_items function that updates information about the number of staked instruments + +**"Swap" -- exchange X tokens** + +```cpp +update_quests(owner, "swap", tokens2receive.amount); +``` + +Added a line to the swap function that updates information about the number of tokens exchanged for resources + +**"Upgrade" -- upgrade an item to level X** + +```cpp +update_quests(owner, "upgrade", new_level, true); +``` + +Added a line to the upgrade_item function, updating information about the level to which the player has raised a certain tool + +**"Tokens" -- stake X tokens** + +```cpp +update_quests(owner, "tokens", quantity.amount); +``` + +Added a line to the increase_tokens_balance function, updating information about the number of staked tokens on the player's balance + +**"Stone/Wood/etc." -- to farm X resource**s + +```cpp +for (const auto& resource_amount_pair : mined_resources) +{ + update_quests(owner, resource_amount_pair.first, resource_amount_pair.second); +} +``` + +We added a line to the claim function. We go through the names of the resources and enter information about how many of them the player has obtained. + +```cpp +void game::update_quests(const name &player, const std::string &type, float update_amount, bool set) +{ + quests_t quests_table(get_self(), get_self().value); + auto player_iter = quests_table.require_find(player.value, "No such quest"); + + quests_table.modify(player_iter, get_self(), [&](auto& row){ + for (auto& quest : row.quests) + { + if (quest.type == type) + { + if (!set) + { + quest.current_amount += update_amount; + } + else + { + quest.current_amount = std::max(quest.current_amount, update_amount); + } + } + } + }); +} + +``` + +This function goes through the player's quests. If the type of quest matches the one we want to update, we update the **current_amount** of this quest. For example, a player has obtained 10 trees and has two quests for farming trees with current values of 20, 100. After calling the function, they will become 30 and 110. + +PS. This article shows simple and robust quest system, that can be updated as we go, as we may not know what types of quests we will need in future. + +**PS. PS.** The [Following link](https://github.com/dapplicaio/GamingQuests) leads us to a repository that corresponds everything described. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part18.md b/docs/build/tutorials/howto-create_farming_game/Part18.md new file mode 100644 index 0000000..b2c9283 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part18.md @@ -0,0 +1,97 @@ +Part 18: GUI for quests and leaderboards +=== + +In our previous article, we looked at swaps, token stakes, and governance mechanisms. + +Building on that foundation, in this article we will also explore leaderboard functionality and delve into quests in the context of the game ecosystem. + +**Leaderboard**s +---------------- + +A leaderboard is essential for tracking and displaying the performance and progress of participants in a gaming environment. It serves as a visual representation of rankings, motivating players to strive for higher positions and encouraging healthy competition. + +The Leaderboard is typically ranked based on the points accumulated by each participant, showing their relative success. Within this framework, it includes areas such as wood, stone, food, and gems collected or earned per hour. + +For example, a player used the claim action and received 100 stones. Then in the table lboards("Stone") in the corresponding field points = points + 100. + +On the UI, the leaderboard looks like this: + +![](/public/assets/images/tutorials/howto-create_farming_game/part18/image1.png) + +All our data is stored in a table of smart contracts, where each scope has a separate table, this data can be retrieved using the leaderboardTable() function. + +That is, the possible scopes are : **stone, wood, food, gems, tokens, miningpwr** + +```js +export const leaderboadrTable = async () => { + const { rows } = await fetchRows({ + contract: "dappgamemine", + scope: "stone", + table: "lboards" + }); + + return rows; +} + +``` + +Here is a link to the leaderboard in the smart contract: + + + +**Quests system** +----------------- + +Each quest is a specific action that the player must perform and the amount of time it takes to complete it. For example, "Farm 10 stones" or "Craft 3 tools".  The quest status is updated with the corresponding action. When the quest conditions are met, the player can complete the quest (remove it from the quest list) and collect the reward (if any). + +Quests allow you to direct the player's gameplay by gradually revealing game mechanics to him or her. For example, at the beginning of the game, quests will be of the type "build a farm", "build a tool", "get 100 units of food", etc. + +On the UI, the leaderboard looks like this: + +![](/public/assets/images/tutorials/howto-create_farming_game/part18/image2.png) + +When the quest is completed, we can call the "Claim" action to collect the reward. To do this, we use the **collectQuest**() function: + +```js +export const collectQuest = async ({ activeUser, index }) => { + return await signTransaction({ + activeUser, + account: 'dappgamemine', + action: 'compltquest', + data: { + player: activeUser.accountName, + quest_index: index, + } + }); +}; +``` + +Here we use an action called "compltquest" for claim reward^ + +- **player**-- this is our nickname of the user who connected his wallet, we take it from activeUser.accountName; +- **quest_index** -- quest ID from "**quests**" table; + +To retrieve all the quests created by the contract, you must call the **fetchQuests**() function: + +```js +export const fetchQuests = async () => { + const { rows } = await fetchRows({ + contract: 'dappgamemine', + scope: 'dappgamemine', + table: "quests" + }); + return rows; +} +``` + +Here is a link for claim completed quest: + + + +and quests table: + + + +In this article we made an overview of final elements of our GUI for WAX game development series. Thus we covered leaderboards and quests as core feature of most of the modern web3 games. + +**PS. PS.** The [Following link](https://github.com/dapplicaio/GUIQuestsLeaderboards) leads us to a repository that corresponds everything described. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part2.md b/docs/build/tutorials/howto-create_farming_game/Part2.md new file mode 100644 index 0000000..aa67f20 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part2.md @@ -0,0 +1,168 @@ +Part 2. Creating a farmable object or item in AtomicAssets standard. +==================================================================== + +This is next article regarding game creation series, you can follow first [introduction one](https://dapplica.io/blog/wax/part-1-how-to-make-a-game-on-wax-general-concepts) if you didn’t read it yet. + +Play-to-Earn (P2E) gaming is taking the web3 and blockchain world by storm, redefining how players interact with games. At the forefront? The WAX blockchain. It’s not just about virtual assets; it’s a hub for profitable farming and trading unique NFTs. WAX is where gaming meets earning, blending fun with financial potential.” + +**Step 1: Crafting Your NFT Collection on WAX** + +Kicking off your NFT collection? Start with a bang by picking a killer theme. Space opera, historical epic, or a fantasy world – the sky’s the limit. Your choice should resonate with your audience. It’s all about creating an NFT universe that captivates and connects. + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/collection_creation-980x517.png) + +Collection creation screen on Atomic Hub + +**1\. Collection Name** + +Time to brand your NFT collection! Pick a name that’s unique and captures your project’s spirit. Keep it within 12 characters, mixing numbers and letters. No spaces allowed. Like ‘galacticgame’. + +#### 2\. Display Name + +This is what players see. Choose something catchy that embodies your game’s vibe. Example: ‘Galactic Game’. + +#### 3\. Website URL + +Add your website link where players and investors can dive deeper into your world. For instance: https://wax.io. + +#### 4\. Market Fee + +Decide on the trading commission for your game’s platform. Let’s say, 2% for trading your NFTs. + +**5\. Collection Description** + +Get creative with your collection’s description. In 3000 characters or less, make it pop with your project’s main themes and ideas. + +#### 6\. Collection Images + +##### Background Image + +Set the scene with a background image that mirrors your game’s mood and theme. + +##### Logo Image + +Pick an iconic logo to represent your collection, making it instantly recognizable. + +#### 7\. Social Media + +Connect with your community. Add all your social links – Twitter, Medium, Facebook, GitHub, Discord, YouTube, Telegram. + +These steps are your blueprint for crafting a standout NFT collection on WAX. Remember, every detail, from the name to socials, shapes your game’s appeal. + +**Step 2. Description of the Process of Creating NFT items on the WAX Testnet** + +**2.1 creation of a category (scheme) for NFT** + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/creatrschema1-980x500.png) + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/createschema2-980x504.png) + +Schema creation screen in Atomic Hub + +We will have following fields: + +**name:** + +Attribute type: string + +Description: Represents the name of the player or object being determined. + +**img :** + +Attribute type: image + +Description: Corresponds to a visual representation or image associated with a player or object. + +**video:** + +Attribute type: string + +Description: Indicates any video content associated with a player or object that provides additional multimedia elements. + +**slots:** + +Attribute type: uint8 + +Description: Shows the current number of available slots for resources, items or other items in the game. + +**maxSlots:** + +Attribute type: uint8 + +Description: Indicates the maximum number of slots that can be obtained or improved in the game. + +**level:** + +Тип атрибуту: uint8 + +Опис: Вказує на рівень чи ранг гравця чи об’єкта, вказуючи на прогрес в грі. + +**upgradable:** + +Attribute type: bool + +Description: A Boolean value indicating whether a player or object can be improved or leveled up. + +**rarity:** + +Attribute type: string + +Description: Describes the rarity level of a player or item, noting its uniqueness or rarity. + +**faction :** + +Attribute type: string + +Description: Represents the faction or group affiliation of a player or object in the game. + +**miningBoost:** + +Attribute type: float + +Description: Indicates any boost or bonus associated with loot associated with a player or object. + +**staking:** + +Attribute Type: int32\[\] + +Description: Calls the staking engine, providing information about the staking capabilities of a player or object. + +**stakeableResources:** + +Attribute Type: string\[\] + +Description: Lists the types of resources that can be placed by a player or an object in the game. + +**2.2 creation of Templates for NFT** + +First we need to choose a category (scheme) + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/selectschema-980x222.png) + +Select schema for Template + +then we need enter all the data for our Template: + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/CreateTemplate1-980x494.png) + +Creating NFT Template via Atomic Hub + +after successful creation, we can see the result, such as below + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/templateCreated-1024x258.png) + +Templates Created for Collection in Atomic Hub + +**2.3 Mint NFT** + +So we need to choose a category (scheme) and a template: + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/createtemplate-result-980x476.png) + +Minting new NFT at Atomic Hub + +then we enter whom to mint the NFT in Asset Receiver field and enter fields for NFT, if necessary: + +![](/public/assets/images/tutorials/howto-create_farming_game/part2/createtemplate3-1-980x481.png) + +This is a brief process of creating one of our main asset types in this series of articles. Now let’s proceed with another type of assets. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part3.md b/docs/build/tutorials/howto-create_farming_game/Part3.md new file mode 100644 index 0000000..99a860b --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part3.md @@ -0,0 +1,132 @@ +Part 3. Farming NFT creation on Atomic Hub +========================================== +In our last article, we walked you through creating a collection and item. Now, let's step up the game and focus on crafting a farming item. + +### 3.1 Creating a Category (Scheme) for NFTs + +It's all about laying the groundwork for your NFTs. We'll dive into setting up a category or scheme that defines how your farming items will function and stand out." + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/creatingSchema-1024x467.png) + +Creating Schema + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/creatingSchemaEnd-1024x427.png) +Creating Schema + +name: + +Attribute Type: string + +Description: Represents the name of the player, character, or object within the game. + +img (image): + +Attribute Type: image + +Description: Corresponds to the visual representation or image associated with the player, character, or object. + +video: + +Attribute Type: string + +Description: Refers to any video content linked to the player, character, or object, providing additional multimedia elements. + +farmResource: + +Attribute Type: string + +Description: Specifies the type of resource that can be farmed or harvested by the player or object. + +amount: + +Attribute Type: float + +Description: Represents the quantity or amount of the farmed resource. + +level: + +Attribute Type: uint8 + +Description: Denotes the level or rank of the player, character, or object, showcasing progression within the farming aspect of the game. + +upgradable: + +Attribute Type: bool + +Description: A boolean value indicating whether the farming resource, player, or object can be upgraded. + +rewardInterval: + +Attribute Type: uint32 + +Description: Indicates the time interval (in seconds) between each reward or harvest cycle. + +rarity: + +Attribute Type: uint32 + +Description: Specifies the rarity level of the player, character, or object in the context of farming, distinguishing it in terms of uniqueness or scarcity. + +faction: + +Attribute Type: string + +Description: Represents the faction or group affiliation of the player or object within the farming aspect of the game. + +lastClaim: + +Attribute Type: uint32 + +Description: Indicates the timestamp or information regarding the last time the resource was claimed or harvested. + +maxLevel: + +Attribute Type: uint32 + +Description: Specifies the maximum level that the farming resource, player, or object can attain. + +initialAmount: + +Attribute Type: uint8 + +Description: Represents the initial quantity or amount of the farmed resource when starting the game or acquiring the object. + +miningRate: + +Attribute Type: float + +Description: Indicates the rate at which the farming resource is generated or harvested over time. + +3.1 Creating NFT Templates + +First things first: select the right category (or scheme) for your NFT template. It's like choosing the perfect mold for your farming item -- critical for shaping its identity and function. + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/create-new-template-1024x524.png) +New Template Creation via Atomic Hub + +Next up, it's time to bring your NFT to life. Upload the image that'll represent your farming item and fill in the necessary fields to give it character and context. + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/temp1-1024x536.png) +Creation of NFT Template + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/temp2-1024x499.png) +this is what the created template looks like: + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/tempres-1024x262.png) +Creation of NFT Template completed + +3.1 Mint NFT + +First, we need to choose a category (scheme) and a template: + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/mintNFT-1024x502.png) +Minting NFT + +Then, decide if need to update the NFT data and consider adding extra details if needed. Keep in mind, the data in your NFT can be static or dynamic, changing as the game progresses or based on other logic. It's like giving your NFT a personality that evolves! + +![](/public/assets/images/tutorials/howto-create_farming_game/part3/mintNFTend-1024x542.png) +Minting NFT and sending to receiver + +After creation, our NFT will be displayed in the user's inventory and can be used in the future in our game. + +In next article we will explain tokens and resources that are needed for our game. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part4.md b/docs/build/tutorials/howto-create_farming_game/Part4.md new file mode 100644 index 0000000..e30d648 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part4.md @@ -0,0 +1,95 @@ +Part 4. What are resources and tokens in our game creation process. +=================================================================== +In this article, we're diving into two game-changers: resources and tokens. + +First up, resources. Think of them as the nuts and bolts of your game's economy. They're the materials and items players mine, produce, or nab during gameplay. Each resource comes with its own flavor -- rarity, crafting abilities, and ways to interact within the game. + +Resources aren't just collectibles; they're tradeable, usable, and crucial for character development and item crafting. In our game, they're the key to swapping for tokens and sprucing up items. And yes, they're neatly stored in a user-specific resource balance table." + +![](/public/assets/images/tutorials/howto-create_farming_game/part4/resourcesandtokens-1024x140.png) + +key_id -- keys for searching in the table, is created using the envelope of the resource name into a number + +amount --  amount of the acquired resource + +resource_name -- name of resource + +Next, let's look at a practical example: code to add a specific resource to a player's table. This code snippet is a real-world application, showing how to update a player's resource balance in the game. + +```C + resources_t resources_table(get_self(), owner.value); +``` + +Now, let's tackle initializing the resource table for a player. Think of it as setting up a personal resource bank account. We use `get_self()` to specify our contract as the table's location, ensuring the resources are linked to the right owner. + +```C + const float amount = 25.3; + const std::string resource_name = "wood"; + const uint64_t key_id = stringToUint64(resource_name); +``` + +Now, let's create our variables to initialize the resource. + +amout --  quantity of mined resource + +resource_name -- name + +key_id --  is generated from a string, that is, converts string into a number for writing to the table + +```C +auto resources_table_itr = resources_table.find(key_id); + if(resources_table_itr == std::end(resources_table)) + { + resources_table.emplace(get_self(), [&](auto &new_row) + { + new_row.key_id = key_id; + new_row.resource_name = resources_table_itr; + new_row.amount = amount; + }); + } + else + { + resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) + { + new_row.amount += amount; + }); + } +``` + +Next, we need to check whether such a key already exists in the table, if it exists, then we just need to edit the table. + +```C +resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) + { + new_row.amount += amount; + }); +``` + +In this segment, we delve into handling the resource table iterator (`resources_table_itr`). This iterator is crucial for updating resource amounts in our contract, denoted by `get_self()`. We use a lambda function for editing the `amount` field, adding the new number to the existing one. + +Additionally, if there's no existing record for a specific resource, it's important to add a new entry to accurately track resources for each player. + +```C + resources_table.emplace(get_self(), [&](auto &new_row) + { + new_row.key_id = key_id; + new_row.resource_name = resources_table_itr; + new_row.amount = amount; + }); + +``` + +In this part, we focus on the parameters for resource management. The first parameter determines who pays for the transaction, crucial for maintaining the game's economy. The second parameter is a lambda function used for adding new records to the resource table. + +It's important to note that resources in this game design are non-transferable between users. Instead, they're convertible into tokens or usable within the game, providing a unique approach to resource management and player interaction. + +Now, let's talk about tokens. +----------------------------- + +Tokens, in the blockchain world, are digital assets representing value or assets. They're versatile, used for digital assets, smart contracts, and more. In gaming, they're the virtual currency for buying, selling, or owning unique in-game items. They empower players with decentralized economy and ownership, enhancing the play-to-earn experience. + +In our game, we use a separate contract for tokens based on the common eosio.token standard on WAX. This setup ensures a smooth integration of tokens into the game environment. + +You can follow link to check standard token implementation  + +In the next article we will cover NFT staking process as preparation to resource or token farming. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part5.md b/docs/build/tutorials/howto-create_farming_game/Part5.md new file mode 100644 index 0000000..fdf25f8 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part5.md @@ -0,0 +1,454 @@ +Part 5. NFT Staking +=================== + +Today, we're diving into staking NFTs in our game's smart contract. This strategy helps manage market prices by locking NFTs in the contract. Imagine setting an unstaking period of 3 to 30 days for added control. + +Staking NFTs simplifies ownership tracking, crucial for rewarding owners periodically, even per blockchain block. This method avoids the need for centralized systems to track ownership, a common challenge with alternatives that rely on transaction history or external APIs. + +Staking NFTs in our game is a straightforward process: + +1. The player picks an NFT to stake. +2. They send this NFT to our contract. +3. Our contract acknowledges and processes the transfer. +4. Finally, the contract logs the player's staked NFT in a table, ready for future interactions. + +This process ensures a seamless and efficient staking experience, integral to the game's dynamics. + +Now lets dig into whole staking source code and example  + +Main file game.hpp  + +```C +#include +#include +#include +#include "atomicassets.hpp" + +using namespace eosio; +``` + +At the top of the file, we connect all the necessary libraries and namespaces + +```C +class [[eosio::contract]] game : public contract +{ + public: + using contract::contract; + private: +}; +``` + +This is what an empty class called a game looks like, in it we will implement all the functions needed for staking.  + +The first step is to add a function for listening to the transfer, make it public:  + +```C + // listening atomicassets transfer + [[eosio::on_notify("atomicassets::transfer")]] + void receive_asset_transfer + ( + const name& from, + const name& to, + std::vector & asset_ids, + const std::string& memo + ); +``` + +Regarding eosio::on_notify you can check more about it here   + +In this function, we configure it to listen to the Atomic Assets contract and its transfer function. Here's a brief rundown: + +- `from`: This represents the player sending the NFT. +- `to`: This should be set to our contract. +- `asset_ids`: These are the gaming NFTs involved in the transaction. +- `memo`: A message included with the transfer. Future memos will be specified to guide our contract on how to process the data. + +This setup is crucial for correctly handling NFT transfers in our game environment. + +```C +//scope: owner + struct [[eosio::table]] staked_j + { + uint64_t asset_id; // item + std::vector staked_items; // farming items + + uint64_t primary_key() const { return asset_id; } + }; + typedef multi_index< "staked"_n, staked_j > staked_t; +``` + +In this part, we've set up a table to keep track of staked NFTs: + +- Scope: Defined by the player's nickname. +- asset_id: Identifies the specific NFT (item). +- staked_items: An array containing the staked NFTs (farming items). +- primary_key: A necessary function in all tables, determining the search key for records. + +Additionally, we've crafted helper functions to enhance the code's readability within the contract. + +```C + void stake_farmingitem(const name& owner, const uint64_t& asset_id); + void stake_items(const name& owner, const uint64_t& farmingitem, const std::vector& items_to_stake); + + // get mutable data from NFT + atomicassets::ATTRIBUTE_MAP get_mdata(atomicassets::assets_t::const_iterator& assets_itr); + // get immutable data from template of NFT + atomicassets::ATTRIBUTE_MAP get_template_idata(const int32_t& template_id, const name& collection_name); + // update mutable data of NFT + void update_mdata(atomicassets::assets_t::const_iterator& assets_itr, const atomicassets::ATTRIBUTE_MAP& new_mdata, const name& owner); +``` + +Now, we're diving deeper into the `game.cpp` file to detail the implementation of the function that monitors atomic transfers. This is where the magic happens in handling NFT transactions within our game's framework. + +```C +void game::receive_asset_transfer +( + const name& from, + const name& to, + std::vector & asset_ids, + const std::string& memo +) +{ + if(to != get_self()) + return; + + if(memo == "stake farming item") + { + check(asset_ids.size() == 1, "You must transfer only one farming item to stake"); + stake_farmingitem(from, asset_ids[0]); + } + else if(memo.find("stake items:") != std::string::npos) + { + const uint64_t farmingitem_id = std::stoll(memo.substr(12)); + stake_items(from, farmingitem_id, asset_ids); + } + else + check(0, "Invalid memo"); + +} +``` + +First, we verify if the NFT was sent to our contract using `get_self()`. Depending on the memo, we distinguish between staking a farming item and other items. + +For a farming item, we confirm that only one NFT is sent, adhering to our game rule of staking one item at a time. Then, we invoke `stake_farmingitem`. + +For staking other items, the memo must include the ID of the farming item where the NFTs are to be staked, formatted as "stake items:id", with the actual ID of the farming item. + +```C +std::stoll(memo.substr(12)); +``` + +Here we parse the id from the string (memo) and then we call the internal function for item staking: + +```C +else + check(0, "Invalid memo"); +``` + +If the transfer to the contract doesn't match the specified memos for staking, the contract will flag an error. This ensures only valid transactions are processed. Next, we'll explore additional functions used in this process, further detailing how the contract operates. + +```C +void game::stake_farmingitem(const name& owner, const uint64_t& asset_id) +{ + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); + + auto farmingitem_mdata = get_mdata(asset_itr); + if(farmingitem_mdata.find("slots") == std::end(farmingitem_mdata)) + { + auto farmingitem_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + check(farmingitem_template_idata.find("maxSlots") != std::end(farmingitem_template_idata), + "Farming item slots was not initialized. Contact ot dev team"); + check(farmingitem_template_idata.find("stakeableResources") != std::end(farmingitem_template_idata), + "stakeableResources items at current farming item was not initialized. Contact to dev team"); + + farmingitem_mdata["slots"] = (uint8_t)1; + farmingitem_mdata["level"] = (uint8_t)1; + + update_mdata(asset_itr, farmingitem_mdata, get_self()); + } + + staked_t staked_table(get_self(), owner.value); + staked_table.emplace(get_self(), [&](auto &new_row) + { + new_row.asset_id = asset_id; + }); +} +``` + +Following is explanation of this function: + +```C +auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.find(asset_id); +``` + +This part covers how we retrieve a record of our contract's balance from the atomicassets table and locate the specific NFT the user wishes to stake. We'll be using functions from the atomicassets namespace. These are detailed in the header files included with the article, providing a straightforward tutorial on working with the atomic assets standard. No deep diving into the code is required; it's designed to be user-friendly for those implementing the atomic assets standard in their projects. + +```C +auto farmingitem_mdata = get_mdata(asset_itr); +``` + +Here we extract the metadata of the NFT for further work with the data located in the NFT: + +```C +atomicassets::ATTRIBUTE_MAP game::get_mdata(atomicassets::assets_t::const_iterator& assets_itr) +{ + auto schemas = atomicassets::get_schemas(assets_itr->collection_name); + auto schema_itr = schemas.find(assets_itr->schema_name.value); + + atomicassets::ATTRIBUTE_MAP deserialized_mdata = atomicdata::deserialize + ( + assets_itr->mutable_serialized_data, + schema_itr->format + ); + + return deserialized_mdata; +} +``` + +this is our data extraction function, this is where the schema(category) is taken: + +```C + auto schemas = atomicassets::get_schemas(assets_itr->collection_name); + auto schema_itr = schemas.find(assets_itr->schema_name.value); +``` + +The process involves passing data to the atomic data deserialization function in atomicdata. We'll include these files with the code for easy reference. Regarding staking, when we receive the metadata of the NFT, we follow specific steps to ensure accurate processing and recording within the contract. + +```C +if(farmingitem_mdata.find("slots") == std::end(farmingitem_mdata)) + { + auto farmingitem_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + check(farmingitem_template_idata.find("maxSlots") != std::end(farmingitem_template_idata), + "Farming item slots was not initialized. Contact ot dev team"); + check(farmingitem_template_idata.find("stakeableResources") != std::end(farmingitem_template_idata), + "stakeableResources items at current farming item was not initialized. Contact to dev team"); + + farmingitem_mdata["slots"] = (uint8_t)1; + farmingitem_mdata["level"] = (uint8_t)1; + + update_mdata(asset_itr, farmingitem_mdata, get_self()); + } + +``` + +When staking an NFT for the first time, we check for a 'slots' field. If it's absent, we follow the game's requirements to initialize fields, setting up slots and the farming item's level. This initialization is crucial only for the first-time staking of an NFT. + +```C + staked_t staked_table(get_self(), owner.value); + staked_table.emplace(get_self(), [&](auto &new_row) + { + new_row.asset_id = asset_id; + }); + +``` + +Next, we record the staked NFT in our table, using the `owner.value` as the scope. This ensures that the entry is user-specific. The `emplace` function then takes over, where the first parameter is the account authorized to pay for the RAM, and the second parameter is a lambda function for adding a new record to the table. + +This sets the stage for detailing the item staking function. + +```C +void game::stake_items(const name& owner, const uint64_t& farmingitem, const std::vector& items_to_stake) +{ + auto assets = atomicassets::get_assets(get_self()); + + staked_t staked_table(get_self(), owner.value); + auto staked_table_itr = staked_table.require_find(farmingitem, "Could not find farming staked item"); + auto asset_itr = assets.find(farmingitem); + + auto farmingitem_mdata = get_mdata(asset_itr); + auto farmingitem_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + + check(std::get(farmingitem_mdata["slots"]) >= staked_table_itr->staked_items.size() + items_to_stake.size(), + "You don't have empty slots on current farming item to stake this amount of items"); + + atomicdata::string_VEC stakeableResources = std::get(farmingitem_template_idata["stakeableResources"]); + for(const uint64_t& item_to_stake : items_to_stake) + { + asset_itr = assets.find(item_to_stake); + auto item_mdata = get_mdata(asset_itr); + + item_mdata["lastClaim"] = current_time_point().sec_since_epoch(); + auto template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + if(item_mdata.find("level") == std::end(item_mdata)) + { + check(template_idata.find("farmResource") != std::end(template_idata), + "farmResource at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("miningRate") != std::end(template_idata), + "miningRate at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("maxLevel") != std::end(template_idata), + "maxLevel at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + + item_mdata["level"] = (uint8_t)1; + } + + check(std::find(std::begin(stakeableResources), std::end(stakeableResources), std::get(template_idata["farmResource"])) != std::end(stakeableResources), + "Item [" + std::to_string(item_to_stake) + "] can not be staked at current farming item"); + update_mdata(asset_itr, item_mdata, get_self()); + } + + staked_table.modify(staked_table_itr, get_self(), [&](auto &new_row) + { + new_row.staked_items.insert(std::end(new_row.staked_items), std::begin(items_to_stake), std::end(items_to_stake)); + }); +} +``` + +Now step by step + +```C + auto assets = atomicassets::get_assets(get_self()); +``` + +here we are receiving NFTs from contract + +```C + staked_t staked_table(get_self(), owner.value); + auto staked_table_itr = staked_table.require_find(farmingitem, "Could not find farming staked item"); +``` + +The process involves extracting the player's table and searching for the specific farming item ID mentioned in the memo. If the specified ID isn't found, the system triggers an error message. + +```C + auto asset_itr = assets.find(farmingitem); +``` + +Next, the process involves locating the NFT in the atomic table to extract its data. + +```C + auto farmingitem_mdata = get_mdata(asset_itr); + auto farmingitem_template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); +``` + +In this step, we extract the NFT metadata and immutable template data. The function `get_template_idata` is used for this purpose, functioning similarly to `get_mdata`. This extraction is vital for accurately understanding and utilizing the NFT's characteristics within the game. + +```C +atomicassets::ATTRIBUTE_MAP game::get_template_idata(const int32_t& template_id, const name& collection_name) +{ + auto templates = atomicassets::get_templates(collection_name); + auto template_itr = templates.find(template_id); + + auto schemas = atomicassets::get_schemas(collection_name); + auto schema_itr = schemas.find(template_itr->schema_name.value); + + return atomicdata::deserialize + ( + template_itr->immutable_serialized_data, + schema_itr->format + ); +} +``` + +In this part, we're extracting information about the NFT template. From this template data, we then pull out the specific details we need. + +```C +check(std::get(farmingitem_mdata["slots"]) >= staked_table_itr->staked_items.size() + items_to_stake.size(), + "You don't have empty slots on current farming item to stake this amount of items"); + +``` + +The next step involves verifying if there's sufficient space in the farming item to store new items. This check is essential to ensure that the item's capacity aligns with the game's rules and mechanics. + +```C +atomicdata::string_VEC stakeableResources = std::get(farmingitem_template_idata["stakeableResources"]); +``` + +In this phase, we utilize a vector or array of types. This is where we'll record all the resources that the player's chosen items are set to farm. + +* * * * * + +```C +for(const uint64_t& item_to_stake : items_to_stake) + { + asset_itr = assets.find(item_to_stake); + auto item_mdata = get_mdata(asset_itr); + + item_mdata["lastClaim"] = current_time_point().sec_since_epoch(); + auto template_idata = get_template_idata(asset_itr->template_id, asset_itr->collection_name); + if(item_mdata.find("level") == std::end(item_mdata)) + { + check(template_idata.find("farmResource") != std::end(template_idata), + "farmResource at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("miningRate") != std::end(template_idata), + "miningRate at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("maxLevel") != std::end(template_idata), + "maxLevel at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + + item_mdata["level"] = (uint8_t)1; + } + + check(std::find(std::begin(stakeableResources), std::end(stakeableResources), std::get(template_idata["farmResource"])) != std::end(stakeableResources), + "Item [" + std::to_string(item_to_stake) + "] can not be staked at current farming item"); + update_mdata(asset_itr, item_mdata, get_self()); + } +``` + +Next, we iterate through the items the player wants to stake, extracting NFT data for each, similar to earlier steps. + +```C +item_mdata["lastClaim"] = current_time_point().sec_since_epoch(); +``` + +We then record the 'last time stamped' field for each item, crucial for future resource farming calculations. This timestamp defaults to the moment the item is processed. + +```C +if(item_mdata.find("level") == std::end(item_mdata)) + { + check(template_idata.find("farmResource") != std::end(template_idata), + "farmResource at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("miningRate") != std::end(template_idata), + "miningRate at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + check(template_idata.find("maxLevel") != std::end(template_idata), + "maxLevel at item[" + std::to_string(item_to_stake) + "] was not initialized. Contact to dev team"); + + item_mdata["level"] = (uint8_t)1; + } +``` + +At this point, we verify if the item is being staked for the first time. If it's the first staking instance and the 'level' field is missing, it indicates that we need to add this field to the NFT. Additionally, we check other mandatory fields in the template to ensure they're properly initialized. + +```C + check(std::find(std::begin(stakeableResources), std::end(stakeableResources), std::get(template_idata["farmResource"])) != std::end(stakeableResources), + "Item [" + std::to_string(item_to_stake) + "] can not be staked at current farming item"); +``` + +In this step, we assess if the farming item can accommodate the staking of an item that mines a specific resource. This involves checking the array of resources that the farming item can mine and ensuring that the items the player wants to stake align with the capabilities of the corresponding farming item. + +```C +update_mdata(asset_itr, item_mdata, get_self()); +``` + +Once we confirm that everything is in order, we proceed to update the NFT metadata as described in the previous steps. This ensures that the NFT is correctly modified to reflect its new status and capabilities within the game's ecosystem. + +```C +void game::update_mdata(atomicassets::assets_t::const_iterator& assets_itr, const atomicassets::ATTRIBUTE_MAP& new_mdata, const name& owner) +{ + action + ( + permission_level{get_self(),"active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "setassetdata"_n, + std::make_tuple + ( + get_self(), + owner, + assets_itr->asset_id, + new_mdata + ) + ).send(); +} +``` + +Next, we call the atomic function, inputting all the relevant data. After updating the NFT metadata, we also make corresponding changes to the staking table. + +```C + staked_table.modify(staked_table_itr, get_self(), [&](auto &new_row) + { + new_row.staked_items.insert(std::end(new_row.staked_items), std::begin(items_to_stake), std::end(items_to_stake)); + }); +``` + +Here we use modify, since such an entry already exists in the table and we just need to change it. The first parameter is an iterator that needs to be changed (an entry in the table), the second is who pays for the RAM, the third is a lambda for editing an entry in the table. + +PS. The [Following link](https://github.com/dapplicaio/StakingNFTS) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. Next articles will contain also past code examples, so our framework will evolve over time containing all past articles. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part6.md b/docs/build/tutorials/howto-create_farming_game/Part6.md new file mode 100644 index 0000000..23e17f7 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part6.md @@ -0,0 +1,236 @@ +Part 6. Types of Farming and farming process +============================================ + +In this article, we'll break down the resource farming process. Building on the staking code from our previous article, we'll introduce additional tables and functions specific to resource farming. + +The first step is adding a table to our code for resource storage. This table is key to managing and tracking the resources players collect and use within the game. + +```C +//scope: owner + struct [[eosio::table]] resources_j + { + uint64_t key_id; + float amount; + std::string resource_name; + + uint64_t primary_key() const { return key_id; } + }; + typedef multi_index< "resources"_n, resources_j > resources_t; +``` + +In the resource farming table, we have the following fields: + +- key_id: This is the unique identifier for each resource, represented as a number to facilitate table searches. +- amount: The quantity of the specific resource. +- resource_name: The name of the resource, such as "stone", "wood", etc. + +Additionally, we'll use a helper function to convert the resource name into a numerical `key_id`. This function simplifies the process of managing and referencing resources in our table. + +```C + +const uint64_t pixelfarm::stringToUint64(const std::string& str) +{ + uint64_t hash = 0; + + if (str.size() == 0) return hash; + + for (int i = 0; i < str.size(); ++i) + { + int char_s = str[i]; + hash = ((hash << 4) - hash) + char_s; + hash = hash & hash; + } + + return hash; +} +``` + +Function, which takes the string `str` as an input parameter and returns a 64-bit unsigned integer of type `uint64_t`. The function uses a simple hashing algorithm to convert a string into a unique integer value. + +Initialize a 64-bit unsigned `hash` variable to 0. + +Check the length of the input string `str`. If it's empty (length 0), return the `hash` value. + +Iterate through each character in `str`. + +Calculate and store the ASCII code of each character in `char_s`. + +Perform the hashing operation: + +Shift `hash` value 4 bits left: `hash << 4`. + +Subtract `hash` from the shifted value: `(hash << 4) - hash`. + +Add the ASCII code of the character: `+ char_s`. + +Apply `hash = hash & hash` to limit the `hash` value to 64 bits and prevent overflow. + +Return the final `hash` value after the loop ends. + +Next, we will incorporate the stamp function into our code for further processing. + +```C +void claim(const name& owner, const uint64_t& farmingitem); +``` + +and support functions for claiming + + +Explanation of above, step by step. + +1\. ```auto item_mdata = get_mdata(assets_itr);``` Getting metadata (metadata) for NFT using the `assets_itr` iterator. + +2\. const uint32_t& lastClaim = std::get(item_mdata["lastClaim"]); Getting the value of "lastClaim" from the NFT metadata. This represents the time of the last output of the resource. + +3\. std::pair mined_resource; Creation of an object of type `std::pair`, which will be used to store the mined resource and its quantity. + +4. if(time_now > lastClaim) { ... }: Checking whether time has passed since the last claim of the resource. If so, additional steps are taken. + +5\. auto item_template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); Obtaining template data (template) for NFT using its identifier and collection name. + +6\. Obtaining the necessary data from the template: + +    -- const float& miningRate = std::get>(item_template_idata["miningRate"]); Getting the resource mining rate. + +    -- const std::string& farmResource = std::get(item_template_idata["farmResource"]); Get the name of the resource to mine. + +7\. const uint8_t& current_lvl = std::get(item_mdata["level"]); Obtaining the current level (level) of NFT from metadata. + +8\. Calculation of the rate of resource extraction according to the level: + +    float miningRate_according2lvl = miningRate; + +    for(uint8_t i = 1; i < current_lvl; ++i) + +        miningRate_according2lvl = miningRate_according2lvl + (miningRate_according2lvl * upgrade_percentage / 100); + +9\. Calculation of the amount of extracted resource: + +    const float& reward = (time_now -- lastClaim) * miningRate_according2lvl; + +10\. item_mdata["lastClaim"] = time_now; Update the value of "lastClaim" in the NFT metadata to the current time. + +11\. update_mdata(assets_itr, item_mdata, get_self()); Update NFT metadata with new value "lastClaim". + +12\. Filling the mined_resource object with data on the mined resource and its quantity. + +13\. return mined_resource; Return of the mined_resource object as a result of the function. + +and an additional function to increase the balance of resources: + +```C +void game::increase_owner_resources_balance(const name& owner, const std::map& resources) +{ + resources_t resources_table(get_self(), owner.value); + for(const auto& map_itr : resources) + { + const uint64_t& key_id = stringToUint64(map_itr.first); + + auto resources_table_itr = resources_table.find(key_id); + if(resources_table_itr == std::end(resources_table)) + { + resources_table.emplace(get_self(), [&](auto &new_row) + { + new_row.key_id = key_id; + new_row.resource_name = map_itr.first; + new_row.amount = map_itr.second; + }); + } + else + { + resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) + { + new_row.amount += map_itr.second; + }); + } + } +} +``` + +1\. resources_t resources_table(get_self(), owner.value); Declaration and initialization of the `resources_t` table object, which is used to store the resources of the game owner (`owner`). A table object is created for the contract using the owner ID. + +2\. for(const auto& map_itr : resources) { ... }: A loop that goes through all key-value pairs in the input dictionary `resources`. + +3\. const uint64_t& key_id = stringToUint64(map_itr.first); Get a unique identifier (`key_id`) for a resource based on the resource name from the input dictionary. The `stringToUint64` function you provided earlier is used. + +4\. auto resources_table_itr = resources_table.find(key_id); Search for an entry in the table by the received `key_id`. + +5\. if(resources_table_itr == std::end(resources_table)) { ... } Checking whether an entry for the specified `key_id` exists in the table. + +6\. If the record is not found (`if' branch): + +    -- resources_table.emplace(get_self(), [&](auto &new_row) { ... }); Adding a new record to the table using the `emplace` function. The record contains a unique `key_id`, the name of the resource and its quantity. + +7\. If the record exists (`else' branch): + +    -- resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) { ... });  + +Modification of an existing entry in the table, increasing its number by the value from the input dictionary. + +Now let's talk about the function of the claim itself: + +```C +void pixelfarm::claim(const name& owner, const uint64_t& farmingitem) +{ + require_auth(owner); + + staked_t staked_table(get_self(), owner.value); + auto staked_table_itr = staked_table.require_find(farmingitem, "Could not find staked farming item"); + auto assets = atomicassets::get_assets(get_self()); + auto assets_itr = assets.find(farmingitem); + + //to get mining boost + auto farmingitem_mdata = get_mdata(assets_itr); + float miningBoost = 1; + if(farmingitem_mdata.find("miningBoost") != std::end(farmingitem_mdata)) + miningBoost = std::get(farmingitem_mdata["miningBoost"]); + + // first - resource name, second - resource amount + std::map mined_resources; + const uint32_t& time_now = current_time_point().sec_since_epoch(); + for(const uint64_t& item_to_collect : staked_table_itr->staked_items) + { + auto assets_itr = assets.find(item_to_collect); + const std::pair item_reward = claim_item(assets_itr, 2, time_now); // 2 is the percentage of increase in mainrate for each level + + if(item_reward != std::pair()) + if(item_reward.second > 0) + mined_resources[item_reward.first] += item_reward.second; + } + check(mined_resources.size() > 0, "Nothing to claim"); + + increase_owner_resources_balance(owner, mined_resources); +} +``` + +1\. require_auth(owner); Checking whether the user who called the function has sufficient authorization rights for the owner (`owner`). + +2\. staked_t staked_table(get_self(), owner.value); Declaration and initialization of the `staked_t` table to track nested elements of the game owner (`owner`). + +3\. auto staked_table_itr = staked_table.require_find(farmingitem, "Could not find staked farming item"); Search for an entry in the table of nested items using the unique identifier `farmingitem`. If the record is not found, an error is generated. + +4\. auto assets = atomicassets::get_assets(get_self()); Get all assets using `atomicassets::get_assets` function. + +5\. `auto assets_itr = assets.find(farmingitem);`: Search for an asset with the unique identifier `farmingitem` in the asset collection. + +6\. `auto farmingitem_mdata = get_mdata(assets_itr);`: Get metadata for the specified asset. + +7\. `float miningBoost = 1;`: Initialize the `miningBoost` variable with the value 1. + +8\. `if(farmingitem_mdata.find("miningBoost") != std::end(farmingitem_mdata)) miningBoost = std::get(farmingitem_mdata["miningBoost"]);`: Checking the presence of the "miningBoost" key in asset metadata and update `miningBoost` if present. + +9\. std::map mined_resources; Creating a dictionary to store mined resources, where the key is the name of the resource, and the value is its quantity. + +10\. const uint32_t& time_now = current_time_point().sec_since_epoch(); Get the current time in seconds since the epoch. + +11\. Cycle for each bid item: + +     -- const std::pair item_reward = claim_item(assets_itr, 2, time_now); Call the `claim_item` function to get a reward for mining from the specified asset. + +     -- if(item_reward != std::pair()) if(item_reward.second > 0) mined_resources[item_reward.first] += item_reward.second; Adding a mined resource to the dictionary ` mined_resources` if the reward has been received and is positive. + +12\. check(mined_resources.size() > 0, "Nothing to claim"); Checking if there is anything to claim using the `check` function. + +13\. increase_owner_resources_balance(owner, mined_resources); Call the `increase_owner_resources_balance` function to increase the resource balance of the game owner. + +PS. Here you can find [link](https://github.com/dapplicaio/FarmingResources) to repository with staking and farming code. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part7.md b/docs/build/tutorials/howto-create_farming_game/Part7.md new file mode 100644 index 0000000..ecd5310 --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part7.md @@ -0,0 +1,311 @@ +Part 7: Creating GUI for WAX game, staking and farming +====================================================== + +This article will walk you through connecting a wallet to the WAX blockchain, integrating it into a ReactJS interface, and reading data from a WAX smart contract table. We'll also cover the NFT staking process and how to claim rewards.   + +Let's start with the procedure on how to connect your web application to WAX blockchain. + +Starting with WAX wallet connection, we utilize the Universal Authenticator Library (UAL) in our ReactJS app for easy wallet integration and authentication. The `UALProvider` component serves as the authentication context provider, facilitating smooth interactions between the app and various blockchain wallets, simplifying the integration process for developers. + +Incorporate the `UALProvider` into your React app to streamline the authentication process, offering a unified interface for various wallet providers. This setup is pivotal for connecting to wallets efficiently. For step-by-step guidance and configurations, [the UAL GitHub repository](https://github.com/EOSIO/ual-reactjs-renderer) is an invaluable resource, providing detailed documentation and examples for React integration. + +UALProvider component: + +- chains={[waxChain]}: Specifies the blockchain configuration. +- authenticators={waxAuthenticators}: Indicates an array of authenticators available for user selection (e.g., Anchor and Wax). +- appName={'AppName'}: Sets the application name used for identification during authentication. + +```js + + + +``` + +Next, we need to configure the actual config file to connect our wallets to WAX. + +- The waxChain object: + +*Contains configuration for connecting to the WAX network, such as the chain ID and RPC server addresses.* + +- Creating instances of Anchor and WAX cloud wallet: + +*const anchor = new Anchor([waxChain], { appName: name })* + +*An Anchor instance is created with the WAX network configuration and the name of the application.* + +*const waxCloudWallet = new Wax([waxChain], { appName: name })* + +*Creates a Wax instance with the same configuration.* + +- Array waxAuthenticators: + +*Defines an array of authenticators to be used based on a condition. Here, we can edit the state of the mainnet or testnet network.* + +Below is a code example of wax.config.js file + +```js +import { Anchor } from 'ual-anchor'; +import { Wax } from '@eosdacio/ual-wax'; + +import { + WAX_CHAIN_ID, + WAX_RPC_ENDPOINTS_HOST, + WAX_RPC_ENDPOINTS_PROTOCOL, +} from '../constants/wax.constants'; + +export const waxChain = { + chainId: WAX_CHAIN_ID, + rpcEndpoints: [ + { + protocol: WAX_RPC_ENDPOINTS_PROTOCOL, + host: WAX_RPC_ENDPOINTS_HOST, + port: '', + }, + ], +}; + +const anchor = new Anchor([waxChain], { appName: 'TestGame' }); +const waxCloudWallet = new Wax([waxChain], { appName: 'TestGame' }); + +export const waxAuthenticators = + process.env.REACT_APP_MAINNET === 'mainnet' + ? [anchor, waxCloudWallet] + : [anchor]; + +``` + +After triggering the `showModal()` method in your user interface, a modal window will appear, displaying the wallets available for connection. This step is crucial for users to choose their preferred wallet for interacting with the WAX blockchain within your application. + +```js +import { UALContext } from 'ual-reactjs-renderer'; +const { activeUser, showModal } = useContext(UALContext); +``` + +![](/public/assets/images/tutorials/howto-create_farming_game/part7/image1.png) + +After connecting to the WAX blockchain, the next step is to read data from a WAX smart contract table.  + +This involves using a function to fetch rows of data from the table. The function `FetchRows` is specifically designed for this purpose, enabling the application to access and display the required data from the blockchain. This process is essential for integrating blockchain data into your application's user interface, providing users with real-time information directly from the WAX blockchain. + +```js +export const getTableData = async ({ contract, scope, table }) => { + const pageSize = 1000; + let lowerBound = 0; + let fetchMore = true; + + const assets = []; + + while (fetchMore) { + // eslint-disable-next-line no-await-in-loop + const { rows, more, next_key } = await fetchRows({ + contract, + scope, + table, + limit: pageSize, + lowerBound, + }); + + assets.push(...rows); + + if (more) lowerBound = next_key; + else fetchMore = false; + } + + return assets; +}; +``` + +The `FetchRows` function, integral for reading data from the WAX blockchain, utilizes a configuration that employs the `rpc` and `get_table_rows` method. This setup facilitates the retrieval of data directly from the specified table, allowing your interface to display the blockchain data dynamically.  + +```js +export const fetchRows = async ({ + contract, + scope, + table, + limit, + lowerBound = null, + upperBound = null, +}) => { + try { + const config = { + json: true, + code: contract, + scope, + table, + limit, + lower_bound: lowerBound, + upper_bound: upperBound, + }; + + if (!lowerBound) delete config['lower_bound']; + + if (!upperBound) delete config['upper_bound']; + + return await rpc.get_table_rows(config); + } catch (e) { + if (!e.message.includes('assertion failure')) { + const isNewNetworkExist = reinitializeRcp(); + + if (!isNewNetworkExist) throw new Error('NetworkError!'); + + return await fetchRows({ + contract, + scope, + table, + limit, + lowerBound, + upperBound, + }); + } else { + throw new Error(e.message); + } + } +}; +``` + +Here is an example: + +Task: Retrieve all user resources. + +To do this, use the **getResources** function + +**activeUser** - user + +**table** - the contract table from which you want to extract data about resources. + +```js +export const getResouces = async ({ activeUser }) => { + const { rows } = await fetchRows({ + contract: GAME_CONTRACT, + scope: activeUser.accountName, + table: 'resources', + limit: 100, + }); + + return rows; +}; + +``` + +NFT Staking +----------- + +Analyzing the staking process involves understanding the specific tools and mechanisms used in the farming item.  + +This process is crucial for optimizing the use of NFTs within the game, ensuring players can stake their assets efficiently and effectively for resource farming or other benefits.  + +![](/public/assets/images/tutorials/howto-create_farming_game/part7/image2.png) + +To stake a farmingItem with your contract, begin by invoking the specific action designed for staking, accompanied by the `signTransaction()` auxiliary function for executing the transaction. This method secures the transaction and ensures that the farmingItem is staked as intended within your smart contract's operational framework. + +```js +export const stakeFarmingTool = async ({ activeUser, selectItem }) => { + return await signTransaction({ + activeUser, + account: 'atomicassets', + action: 'transfer', + data: { + from: activeUser.accountName, + to: GAME_CONTRACT, + asset_ids: [selectItem], + memo: `stake farming item` + } + }); +}; +``` + +```js +export const signTransaction = async ({ + activeUser, + account, + action, + data, +}) => { + await activeUser.signTransaction( + { + actions: [ + { + account, + name: action, + authorization: [ + { + actor: activeUser.accountName, + permission: 'active', + }, + ], + data, + }, + ], + }, + { + blocksBehind: 3, + expireSeconds: 30, + } + ); +}; + +``` + +Once your farmingItem is successfully staked, the next step is to stake your items (tools). For this action, you'll refer to the specific function provided in the smart contract. This action allows for the staking of tools, further engaging with the game's mechanics and enhancing your in-game assets' utility. + +For detailed instructions or to execute this action, you would typically refer to the provided link to the smart contract. + + + +The action for staking tools involves specifying the `wpId`, which identifies the farmingItem into which the tool is being staked. This parameter is crucial for directing the staking process to the correct item, ensuring that the tool enhances the intended farmingItem's capabilities or benefits within the game. + +```js +export const stakeTool = async ({ activeUser, selectItem, wpId }) => { + return await signTransaction({ + activeUser, + account: 'atomicassets', + action: 'transfer', + data: { + from: activeUser.accountName, + to: GAME_CONTRACT, + asset_ids: [selectItem], + memo: `stake items:${wpId}` + } + }); +}; +``` + +The final step involves claiming resources earned through your staked farm item and tools.  + +After staking your farming item and utilizing tools from your inventory, resources accumulate over time, available for claim every hour from any tool-equipped farm. The user interface displays the accumulated resources, indicating the amount you can claim, making it easy to track your progress and rewards within the game. + +![](/public/assets/images/tutorials/howto-create_farming_game/part7/image3.png) + +The claim action itself has the following fields: + +**action** -- the name of the action in the contract + +**owner**, **farmingitem** -- fields that are sent to the contract + +```js +export const claimRes = async ({ activeUser }) => { + return await signTransaction({ + activeUser, + account: GAME_CONTRACT, + action: 'claim', + data: { + owner: activeUser.accountName, + farmingitem: 1, + }, + }); + }; +``` + +Here is a link to the action in the smart contract for claim rewards: + + + +This article has comprehensively outlined the principles and objects defined at the smart contract level, focusing on how to effectively implement a user interface for these elements in ReactJS.  + +By integrating UI components, developers can create a seamless and interactive experience that allows users to engage with the smart contract's functionalities, such as staking items and claiming resources, directly within a ReactJS application. + +PS. The [Following link](https://github.com/dapplicaio/UIForFarmingandStaking) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part8.md b/docs/build/tutorials/howto-create_farming_game/Part8.md new file mode 100644 index 0000000..6e14cdf --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part8.md @@ -0,0 +1,358 @@ +Part 8: Upgrades of game items in WAX games +=========================================== + +In previous articles, we explored how to farm resources using our NFT. Now, we'll delve into upgrading items for more efficient resource mining. Upgrading items involves enhancing their capabilities, making them more effective at mining resources. This process is key to progressing and achieving higher efficiency in the game. We'll start by adding the necessary code to implement item upgrades, focusing on improving the performance of our NFTs for resource collection. + +```C +void game::upgradeitem( + const name& owner, + const uint64_t& item_to_upgrade, + const uint8_t& next_level, + const uint64_t& staked_at_farmingitem +) +{ + require_auth(owner); + + const int32_t& time_now = current_time_point().sec_since_epoch(); + + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.require_find(item_to_upgrade, ("Could not find staked item[" + std::to_string(item_to_upgrade) +"]").c_str()); + + staked_t staked_table(get_self(), owner.value); + auto staked_table_itr = staked_table.require_find(staked_at_farmingitem, "Could not find staked farming item"); + + check(std::find(std::begin(staked_table_itr->staked_items), std::end(staked_table_itr->staked_items), item_to_upgrade) != std::end(staked_table_itr->staked_items), + "Item [" + std::to_string(item_to_upgrade) + "] is not staked at farming item"); + + //claiming mined resources before upgrade + const std::pair item_reward = claim_item(asset_itr, 2, time_now); // 2 is the percentage of increase in mine rate for each level + if(item_reward != std::pair()) + { + if(item_reward.second > 0) + { + increase_owner_resources_balance(owner, std::map({item_reward})); + } + } + // upgrading + upgrade_item(asset_itr, 2, owner, next_level, time_now); // 2 is the percentage of increase in mine rate for each level +} +``` + +1\. Authentication confirmation: + +require_auth(owner);  + +Checking that the transaction is authorized by the owner (`owner`). + +2\. Getting the current time: + +const int32_t& time_now = current_time_point().sec_since_epoch(); + +Gets the current time in seconds from the epoch. + +3\. Obtaining assets and checking the availability of an asset for an upgrade: + +   auto assets     = atomicassets::get_assets(get_self()); + +   auto asset_itr  = assets.require_find(item_to_upgrade, ("Could not find staked item[" + std::to_string(item_to_upgrade) +"]").c_str()); + +Obtaining assets and verifying the existence of an asset with an identifier 'item_to_upgrade'. + +4\. Obtaining the staking table and checking the availability of the upgrade stake: + +   staked_t staked_table(get_self(), owner.value); + +   auto staked_table_itr = staked_table.require_find(staked_at_farmingitem, "Could not find staked farming item"); + +Get the staking table for the owner and check for the existence of a stake that matches the upgrade. + +5\. Checking the presence of an asset in the stake: + +   check(std::find(std::begin(staked_table_itr->staked_items), std::end(staked_table_itr->staked_items), item_to_upgrade) != std::end(staked_table_itr->staked_items), "Item [" + std::to_string(item_to_upgrade) + "] is not staked at farming item"); + +Checking that the asset to be upgraded is actually in the stack. + +6\. Collecting of mined resources before upgrading: + +   const std::pair item_reward = claim_item(asset_itr, 2, time_now); + +   if(item_reward != std::pair()) { + +       if(item_reward.second > 0) + +       { increase_owner_resources_balance(owner, std::map({item_reward})); } } + +  Call the `claim_item` function to collect mined resources before upgrading. If a reward is received, it is added to the owner's balance. + +7\. Item upgrade: + +   upgrade_item(asset_itr, 2, owner, next_level, time_now); + +Call the `upgrade_item` function to upgrade the asset. In this case, the upgrade is carried out with an increase in mining speed by 2% for each level. + +Alongside detailing the item upgrade process, we'll also outline the auxiliary functions involved. These functions are crucial for implementing upgrades effectively, facilitating the enhancement of items' resource mining capabilities. + +```C +void game::upgrade_item( + atomicassets::assets_t::const_iterator& assets_itr, + const uint8_t& upgrade_percentage, + const name& owner, + const uint8_t& new_level, + const uint32_t& time_now +) +{ + auto mdata = get_mdata(assets_itr); + auto template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); + + const float& mining_rate = std::get>(template_idata["miningRate"]); + const uint8_t& current_lvl = std::get(mdata["level"]); + const std::string& resource_name = std::get(template_idata["farmResource"]); + check(current_lvl < new_level, "New level must be higher then current level"); + check(new_level <= std::get(template_idata["maxLevel"]), "New level can not be higher then max level"); + check(std::get(mdata["lastClaim"]) < time_now, "Item is upgrading"); + + float miningRate_according2lvl = mining_rate; + for(uint8_t i = 1; i < new_level; ++i) + miningRate_according2lvl = miningRate_according2lvl + (miningRate_according2lvl * upgrade_percentage / 100); + + const int32_t& upgrade_time = get_upgrading_time(new_level) - get_upgrading_time(current_lvl); + const float& resource_price = upgrade_time * miningRate_according2lvl; + + std::get(mdata["level"]) = new_level; + std::get(mdata["lastClaim"]) = time_now + upgrade_time; + + reduce_owner_resources_balance(owner, std::map({{resource_name, resource_price}})); + + update_mdata(assets_itr, mdata, get_self()); +} +``` + +1\. Obtaining metadata and template data: + +   auto mdata  = get_mdata(assets_itr); + +   auto template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); + +Obtaining metadata and template data for asset processing. + +2\. Obtaining the required values: + + ```C +   const float& mining_rate   = std::get<(template_idata["miningRate"]); + +   const uint8_t& current_lvl = std::get(mdata["level"]); + +   const std::string& resource_name = std::get(template_idata["farmResource"]); +``` + +Obtaining the mining rate, current level and resource name for a given asset. + +3\. Checking conditions: + +   check(current_lvl < new_level, "New level must be higher than the current level"); + +   check(new_level <= std::get(template_idata["maxLevel"]), "New level cannot be higher than the max level"); + +   check(std::get(mdata["lastClaim"]) < time_now, "Item is upgrading"); + +Checking a number of conditions that must be met before upgrading an asset. + +4\. Calculation of new mining speed values: + +   float miningRate_according2lvl = mining_rate; + +   for(uint8_t i = 1; i < new_level; ++i) + +     miningRate_according2lvl = miningRate_according2lvl + (miningRate_according2lvl * upgrade_percentage / 100); + +  Calculation of the new mining speed, taking into account the percentage of improvement for each level. + +5\. Calculation of the cost of the upgrade: + +   const int32_t& upgrade_time  = get_upgrading_time(new_level) -- get_upgrading_time(current_lvl); + +   const float& resource_price = upgrade_time * miningRate_according2lvl; + +   Calculation of upgrade time and upgrade cost based on the difference between the upgrade times of the new and current levels. + +6\. Updating data and reducing the balance of the owner: + +   std::get(mdata["level"]) = new_level; + +   std::get(mdata["lastClaim"]) = time_now + upgrade_time; + +   reduce_owner_resources_balance(owner, std::map({{resource_name, resource_price}})); + +   update_mdata(assets_itr, mdata, get_self()); + +   Update asset data, set new level and time of last upgrade. Reduction of the owner's balance by the cost of the upgrade. Updating the asset data table. + +In addition to item upgrades, the code below includes a mechanism to deduct resources from the player's balance as payment for the upgrade. + +```C +void game::reduce_owner_resources_balance(const name& owner, const std::map& resources) +{ + resources_t resources_table(get_self(), owner.value); + + for(const auto& map_itr : resources) + { + const uint64_t& key_id = stringToUint64(map_itr.first); + auto resources_table_itr = resources_table.require_find(key_id, + ("Could not find balance of " + map_itr.first).c_str()); + check(resources_table_itr->amount >= map_itr.second, ("Overdrawn balance: " + map_itr.first).c_str()); + + if(resources_table_itr->amount == map_itr.second) + resources_table.erase(resources_table_itr); + else + { + resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) + { + new_row.amount -= map_itr.second; + }); + } + } +} +``` + +1\. Declaration and initialization of the resource table: + +   resources_t resources_table(get_self(), owner.value); + +Creating a table object `resources_table` to store the balance of resources for a specific owner (`owner`). + +2\. Resource loop: + +   for(const auto& map_itr : resources) + +   { + +     // ... + +   } + +  The upgrade process involves a loop that goes through each resource listed in a map (resources). In this map, each resource's name is the key, and the amount to be deducted from the player's balance is the value. This step is crucial for accurately adjusting the player's resource balance according to the costs of the upgrades they wish to apply to their items. + +3\. Obtaining the key and searching in the resource table: + +   const uint64_t& key_id = stringToUint64(map_itr.first); + +   auto resources_table_itr = resources_table.require_find(key_id, ("Could not find balance of " + map_itr.first).c_str()); + +   Obtaining a unique identifier (`key_id`) from a string and using it to find the corresponding entry in the `resources_table` table. + +4\. Balance sufficiency check: + +   check(resources_table_itr->amount >= map_itr.second, ("Overdrawn balance: " + map_itr.first).c_str()); + +   Checking whether there are enough funds on the balance to carry out the withdrawal. + +5\. Updating the resource table: + +   if(resources_table_itr->amount == map_itr.second) + +     resources_table.erase(resources_table_itr); + +   else { + +     resources_table.modify(resources_table_itr, get_self(), [&](auto &new_row) { + +       new_row.amount -= map_itr.second; }); + +   } + +   -- If the amount of the resource on the balance is equal to the amount to be subtracted (`map_itr.second`), then the corresponding record is deleted from the table. + +   -- Otherwise, the amount of the resource is updated, reducing it by the appropriate amount. The `modify' function updates an entry in the table. + +Now let's describe how the farming item is upgraded, Let's add the following fragments to our code: + +```C +void game::upgfarmitem(const name& owner, const uint64_t& farmingitem_to_upgrade, const bool& staked) +{ + require_auth(owner); + + if(staked) + { + auto assets = atomicassets::get_assets(get_self()); + auto asset_itr = assets.require_find(farmingitem_to_upgrade, ("Could not find staked item[" + std::to_string(farmingitem_to_upgrade) +"]").c_str()); + + staked_t staked_table(get_self(), owner.value); + staked_table.require_find(farmingitem_to_upgrade, "Could not find staked farming item"); + + upgrade_farmingitem(asset_itr, get_self()); + } + else + { + auto assets = atomicassets::get_assets(owner); + auto asset_itr = assets.require_find(farmingitem_to_upgrade, ("You do not own farmingitem[" + std::to_string(farmingitem_to_upgrade) +"]").c_str()); + upgrade_farmingitem(asset_itr, owner); + } +``` + +Let's look at the main steps of the function: + +1\. Authentication confirmation: + +   require_auth(owner); + +  Checking that the transaction is authorized by the owner (`owner`). + +2\. Checking whether the oil is stuck: + +   if(staked) { + +     auto assets  = atomicassets::get_assets(get_self()); + +     auto asset_itr  = assets.require_find(farmingitem_to_upgrade, ("Could not find staked item[" + std::to_string(farmingitem_to_upgrade) +"]").c_str()); + +     staked_t staked_table(get_self(), owner.value); + +     staked_table.require_find(farmingitem_to_upgrade, "Could not find staked farming item");    } + +   else  { + +     auto assets = atomicassets::get_assets(owner); + +     auto asset_itr  = assets.require_find(farmingitem_to_upgrade, ("You do not own farmingitem[" + std::to_string(farmingitem_to_upgrade) +"]").c_str()); + +   } + +3\. Upgrade item farming item: + +   upgrade_farmingitem(asset_itr, get_self()); + +   Call the `upgrade_farmingitem` function to upgrade a farming item. The function takes an iterator to NFT and the name of the owner. + +```C +void pixelfarm::upgrade_farmingitem(atomicassets::assets_t::const_iterator& assets_itr, const name& owner) +{ + auto mdata = get_mdata(assets_itr); + auto template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); + + check(std::get(mdata["slots"])++ < std::get(template_idata["maxSlots"]), "Farmingitem has max slots"); + + update_mdata(assets_itr, mdata, owner); +} +``` + +1\. Getting metadata and data templates: + +   auto mdata  = get_mdata(assets_itr); + +   auto template_idata = get_template_idata(assets_itr->template_id, assets_itr->collection_name); + +2\. Checking the number of slots: + +   check(std::get(mdata["slots"])++ < std::get(template_idata["maxSlots"]), "Farmingitem has max slots"); + +   Checking whether the number of farm item slots after the increment does not exceed the maximum number of slots defined in the nft template. Note that a postfix increment is used here (`++` before `std::get(mdata["slots"])`), so the slots value will be incremented before the comparison. + +3\. Farming item data update: + +   update_mdata(assets_itr, mdata, owner); + +   Updating the farm item metadata in the table using the `update_mdata` function. This feature seems to be used to save new data after an upgrade. + +In the next article we will cover a similar function, which is creating new items, so called blends.  + +PS. The [Following link](https://github.com/dapplicaio/GamItemUpgrades) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/Part9.md b/docs/build/tutorials/howto-create_farming_game/Part9.md new file mode 100644 index 0000000..63fba9b --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/Part9.md @@ -0,0 +1,252 @@ +Part 9: Blends of NFTs for WAX games +==================================== + +In this article, following our previous one  on upgrading farming items, we're diving into creating blends. Blending involves combining specific items to create new or enhanced ones within the game. This feature adds depth and strategy to the game, offering players the opportunity to craft unique items with potentially higher value or utility.  + +We'll outline the necessary code additions to implement this functionality, enriching the game's interactive elements and player engagement. + +```C + //scope:contract + struct [[eosio::table]] blends_j + { + uint64_t blend_id; + std::vector blend_components; + int32_t resulting_item; + + uint64_t primary_key() const { return blend_id; } + }; + typedef multi_index< "blends"_n, blends_j > blends_t; +``` + +Introducing a new table for saving blend recipes enriches the game by allowing the creation of unique items through blending. Admins can define recipes, which players then use to blend items, specifying: + +**-- **blend_id**:** A unique identifier for each recipe. + +**-- **blend_components**:** The list of NFT templates required for the blend. + +**-- **resulting_item**:** The ID of the new NFT template created from the blend. + +This feature adds a strategic layer, encouraging players to collect specific NFTs to create more valuable or powerful items. + +Adding a function for the contract admin to create new blends is a strategic development step. + +```C +void game::addblend( + const std::vector blend_components, + const int32_t resulting_item +) +{ + require_auth(get_self()); + + blends_t blends_table(get_self(), get_self().value); + + const uint64_t new_blend_id = blends_table.available_primary_key(); + + blends_table.emplace(get_self(), [&](auto new_row) + { + new_row.blend_id = new_blend_id; + new_row.blend_components = blend_components; + new_row.resulting_item = resulting_item; + }); +} +``` + +```C + require_auth(get_self()); +``` + +Implementing this function ensures that only the contract administrator has the authority to create new blends. + +```C +const uint64_t new_blend_id = blends_table.available_primary_key(); +``` + +The `available_primary_key` function is designed to provide a unique, incremental key for new entries in a table. If existing keys in the table are 1, 5, and 8, the function will return 9, ensuring that each new entry receives a distinct identifier.  + +```C + blends_table.emplace(get_self(), [&](auto new_row) + { + new_row.blend_id = new_blend_id; + new_row.blend_components = blend_components; + new_row.resulting_item = resulting_item; + }); +``` + +Creating a new record in the table involves specifying the necessary fields as previously described.  + +To integrate blending into the game, we'll expand the `receive_asset_transfer` function, previously utilized for staking items. This enhancement involves adding conditions to the if statement to recognize and process blend transactions.  + +```C + else if(memo.find("blend:") != std::string::npos) + { + const uint64_t blend_id = std::stoll(memo.substr(6)); + blend(from, asset_ids, blend_id); + } +``` + +For blending, players need to transfer their NFTs to the contract, similar to staking, but must include a specific memo indicating the blend action and the blend_id, like "blend:blend_id" (for example, "blend:18"). This method ensures the contract recognizes the player's intent to blend items using the specified recipe. + +```C + const uint64_t blend_id = std::stoll(memo.substr(6)); +``` + +Extracting the blend ID from the memo is a crucial step in the blending process, allowing the contract to identify which blend recipe the player intends to use. + +```C +blend(from, asset_ids, blend_id); +``` + +After extracting the blend ID from the memo, the next step involves calling an auxiliary function.  + +```C +void game::blend(const name& owner, const std::vector asset_ids, const uint64_t& blend_id) +{ + auto assets = atomicassets::get_assets(get_self()); + auto templates = atomicassets::get_templates(get_self()); + blends_t blends_table(get_self(), get_self().value); + auto blends_table_itr = blends_table.require_find(blend_id, "Could not find blend id"); + check(blends_table_itr->blend_components.size() == asset_ids.size(), "Blend components count mismatch"); + + std::vector temp = blends_table_itr->blend_components; + for(const uint64_t& asset_id : asset_ids) + { + auto assets_itr = assets.find(asset_id); + check(assets_itr->collection_name == name("collname"), // replace collection with your collection name to check for fake nfts + ("Collection of asset [" + std::to_string(asset_id) + "] mismatch").c_str()); + auto found = std::find(std::begin(temp), std::end(temp), assets_itr->template_id); + if(found != std::end(temp)) + temp.erase(found); + + action + ( + permission_level{get_self(),"active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "burnasset"_n, + std::make_tuple + ( + get_self(), + asset_id + ) + ).send(); + } + check(temp.size() == 0, "Invalid blend components"); + + auto templates_itr = templates.find(blends_table_itr->resulting_item); + + action + ( + permission_level{get_self(),"active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "mintasset"_n, + std::make_tuple + ( + get_self(), + get_self(), + templates_itr->schema_name, + blends_table_itr->resulting_item, + owner, + (atomicassets::ATTRIBUTE_MAP) {}, //immutable_data + (atomicassets::ATTRIBUTE_MAP) {}, //mutable data + (std::vector ) {} // token back + ) + ).send(); +} +``` + +Here's description of what code above does: + +1. The process involves extracting detailed information about NFTs currently held on the contract. This step is essential to later identify the NFTs sent by the user, allowing for the extraction of all relevant template data necessary for the blending process.  + +```C +auto assets = atomicassets::get_assets(get_self()); + auto templates = atomicassets::get_templates(get_self()); + +``` + +2\. First, the process involves checking the blends table to verify if the player's specified blend exists; if not, an error is thrown. Then, it's essential to confirm that the player has submitted the correct quantity of NFTs required for the blend, ensuring compliance with the blend recipe's specifications.  + +```C +blends_t blends_table(get_self(), get_self().value); + auto blends_table_itr = blends_table.require_find(blend_id, "Could not find blend id"); + check(blends_table_itr->blend_components.size() == asset_ids.size(), "Blend components count mismatch"); +``` + +3\. A temporary variable is created to ensure the player has submitted the correct components for the blend. This step is followed by iterating over all the NFTs the player has provided, checking each against the blend's requirements. + +```C +std::vector temp = blends_table_itr->blend_components; + for(const uint64_t& asset_id : asset_ids) + { + .......... + } +``` + +4\. A code that is inside the loop: + +```C + auto assets_itr = assets.find(asset_id); + check(assets_itr->collection_name == name("collname"), // replace collection with your collection name to check for fake nfts + ("Collection of asset [" + std::to_string(asset_id) + "] mismatch").c_str()); +``` + +During the blend verification process, information about each submitted NFT is extracted to ensure it belongs to the game's collection. This involves checking if the NFT's collection name matches the game's specified collection name, which requires replacing "collnamed" with the actual name of your collection in the code.  + +5\. The blending process involves a meticulous check against the blend recipe, using a temporary variable that contains the ID templates of the blend. For each submitted NFT, the system verifies its template against the temporary variable. If a match is found, that template is removed from the variable to prevent duplicates and identify any incorrect templates submitted by the player.  + +Following this verification, the burning function is called. + +```C +auto found = std::find(std::begin(temp), std::end(temp), assets_itr->template_id); + if(found != std::end(temp)) + temp.erase(found); + + action + ( + permission_level{get_self(),"active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "burnasset"_n, + std::make_tuple + ( + get_self(), + asset_id + ) + ).send(); +``` + +6\. Code after the loop: + +```C +check(temp.size() == 0, "Invalid blend components"); +``` + +Next step in the blending process is to ensure that the temporary vector, which contains the components of the blend recipe, is empty. This checks that the player has correctly reset all the required components for the blend.  + +If the vector is empty, it confirms that all components were correctly submitted and processed, allowing the blend to be completed successfully. + +Then there is a search for the resulting template in the table of atoms and creation (mint) of NFT with the required template. + +```C +auto templates_itr = templates.find(blends_table_itr->resulting_item); + +action +( + permission_level{get_self(),"active"_n}, + atomicassets::ATOMICASSETS_ACCOUNT, + "mintasset"_n, + std::make_tuple + ( + get_self(), + get_self(), + templates_itr->schema_name, + blends_table_itr->resulting_item, + owner, + (atomicassets::ATTRIBUTE_MAP) {}, //immutable_data + (atomicassets::ATTRIBUTE_MAP) {}, //mutable data + (std::vector) {} // token back + ) +).send(); +``` + +This article has guided you through the process of creating blends in a game, from setting up blend recipes to verifying player submissions and minting new NFTs. It covers checking NFT components against blend requirements, ensuring all components are correctly submitted, and concluding with the minting of a new item that results from the blend. This blending mechanism enriches the gameplay by allowing players to combine NFTs into new, more valuable assets, fostering a deeper engagement with the game's ecosystem. + +**PS.** The [Following link](https://github.com/dapplicaio/GamingItemBlend) leads us to a repository that corresponds everything described, so you can simply build that code and use in a way you want. \ No newline at end of file diff --git a/docs/build/tutorials/howto-create_farming_game/index.md b/docs/build/tutorials/howto-create_farming_game/index.md new file mode 100644 index 0000000..9c5386d --- /dev/null +++ b/docs/build/tutorials/howto-create_farming_game/index.md @@ -0,0 +1,2 @@ +How to create farming game +=== \ No newline at end of file