Contract Diff Checker

Contract Name:
ProxyControlled

Contract Source Code:

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IControllable {

  function VERSION() external pure returns (string memory);

  function revision() external view returns (uint);

  function previousImplementation() external view returns (address);

  function isController(address contract_) external view returns (bool);

  function isGovernance(address contract_) external view returns (bool);

  function created() external view returns (uint256);

  function createdBlock() external view returns (uint256);

  function controller() external view returns (address);

  function increaseRevision(address oldLogic) external;

}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IProxyControlled {

  function upgrade(address newImplementation_) external;

  function implementation() external view returns (address);

}

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM
 * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to
 * be specified by overriding the virtual {_implementation} function.
 *
 * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
 * different contract through the {_delegate} function.
 *
 * The success and return data of the delegated call will be returned back to the caller of the proxy.
 */
abstract contract Proxy {
  /**
   * @dev Delegates the current call to `implementation`.
   *
   * This function does not return to its internall call site, it will return directly to the external caller.
   */
  function _delegate(address implementation) internal virtual {
    assembly {
    // Copy msg.data. We take full control of memory in this inline assembly
    // block because it will not return to Solidity code. We overwrite the
    // Solidity scratch pad at memory position 0.
      calldatacopy(0, 0, calldatasize())

    // Call the implementation.
    // out and outsize are 0 because we don't know the size yet.
      let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)

    // Copy the returned data.
      returndatacopy(0, 0, returndatasize())

      switch result
      // delegatecall returns 0 on error.
      case 0 {
        revert(0, returndatasize())
      }
      default {
        return (0, returndatasize())
      }
    }
  }

  /**
   * @dev This is a virtual function that should be overriden so it returns the address to which the fallback function
   * and {_fallback} should delegate.
   */
  function _implementation() internal view virtual returns (address);

  /**
   * @dev Delegates the current call to the address returned by `_implementation()`.
   *
   * This function does not return to its internall call site, it will return directly to the external caller.
   */
  function _fallback() internal virtual {
    _beforeFallback();
    _delegate(_implementation());
  }

  /**
   * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other
   * function in the contract matches the call data.
   */
  fallback() external payable virtual {
    _fallback();
  }

  /**
   * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data
   * is empty.
   */
  receive() external payable virtual {
    _fallback();
  }

  /**
   * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback`
   * call, or as part of the Solidity `fallback` or `receive` functions.
   *
   * If overriden should call `super._beforeFallback()`.
   */
  function _beforeFallback() internal virtual {}
}

// SPDX-License-Identifier: BUSL-1.1
/**
            ▒▓▒  ▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓▓▒     ▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓██▓
             ▒██▒▓▓▓▓█▓██████████████████▓  ▒▒▒▓███████████████▒
              ▒██▒▓█████████████████████▒ ▒▓██████████▓███████
               ▒███████████▓▒                   ▒███▓▓██████▓
                 █████████▒                     ▒▓▒▓███████▒
                  ███████▓      ▒▒▒▒▒▓▓█▓▒     ▓█▓████████
                   ▒▒▒▒▒   ▒▒▒▒▓▓▓█████▒      ▓█████████▓
                         ▒▓▓▓▒▓██████▓      ▒▓▓████████▒
                       ▒██▓▓▓███████▒      ▒▒▓███▓████
                        ▒███▓█████▒       ▒▒█████▓██▓
                          ██████▓   ▒▒▒▓██▓██▓█████▒
                           ▒▒▓▓▒   ▒██▓▒▓▓████████
                                  ▓█████▓███████▓
                                 ██▓▓██████████▒
                                ▒█████████████
                                 ███████████▓
      ▒▓▓▓▓▓▓▒▓                  ▒█████████▒                      ▒▓▓
    ▒▓█▒   ▒▒█▒▒                   ▓██████                       ▒▒▓▓▒
   ▒▒█▒       ▓▒                    ▒████                       ▒▓█▓█▓▒
   ▓▒██▓▒                             ██                       ▒▓█▓▓▓██▒
    ▓█▓▓▓▓▓█▓▓▓▒        ▒▒▒         ▒▒▒▓▓▓▓▒▓▒▒▓▒▓▓▓▓▓▓▓▓▒    ▒▓█▒ ▒▓▒▓█▓
     ▒▓█▓▓▓▓▓▓▓▓▓▓▒    ▒▒▒▓▒     ▒▒▒▓▓     ▓▓  ▓▓█▓   ▒▒▓▓   ▒▒█▒   ▒▓▒▓█▓
            ▒▒▓▓▓▒▓▒  ▒▓▓▓▒█▒   ▒▒▒█▒          ▒▒█▓▒▒▒▓▓▓▒   ▓██▓▓▓▓▓▓▓███▓
 ▒            ▒▓▓█▓  ▒▓▓▓▓█▓█▓  ▒█▓▓▒          ▓▓█▓▒▓█▓▒▒   ▓█▓        ▓███▓
▓▓▒         ▒▒▓▓█▓▒▒▓█▒   ▒▓██▓  ▓██▓▒     ▒█▓ ▓▓██   ▒▓▓▓▒▒▓█▓        ▒▓████▒
 ██▓▓▒▒▒▒▓▓███▓▒ ▒▓▓▓▓▒▒ ▒▓▓▓▓▓▓▓▒▒▒▓█▓▓▓▓█▓▓▒▒▓▓▓▓▓▒    ▒▓████▓▒     ▓▓███████▓▓▒
*/
pragma solidity 0.8.23;


import "./UpgradeableProxy.sol";
import "../interfaces/IControllable.sol";
import "../interfaces/IProxyControlled.sol";

/// @title EIP1967 Upgradable proxy implementation.
/// @dev Only Controller has access and should implement time-lock for upgrade action.
/// @author belbix
contract ProxyControlled is UpgradeableProxy, IProxyControlled {

  /// @notice Version of the contract
  /// @dev Should be incremented when contract changed
  string public constant PROXY_CONTROLLED_VERSION = "1.0.0";


  constructor(address _logic) UpgradeableProxy(_logic) {
    //make sure that given logic is controllable
    require(IControllable(_logic).created() >= 0);
  }

  /// @notice Upgrade contract logic
  /// @dev Upgrade allowed only for Controller and should be done only after time-lock period
  /// @param newImplementation_ Implementation address
  function upgrade(address newImplementation_) external override {
    require(IControllable(address(this)).isController(msg.sender), "Proxy: Forbidden");
    IControllable(address(this)).increaseRevision(_implementation());
    _upgradeTo(newImplementation_);
    // the new contract must have the same ABI and you must have the power to change it again
    require(IControllable(address(this)).isController(msg.sender), "Proxy: Wrong implementation");
  }

  /// @notice Return current logic implementation
  function implementation() external override view returns (address) {
    return _implementation();
  }
}

// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import "../openzeppelin/Proxy.sol";

/// @title OpenZeppelin https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/proxy/UpgradeableProxy.sol
/// @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an
///      implementation address that can be changed. This address is stored in storage in the location specified by
///      https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the
///      implementation behind the proxy.
///      Upgradeability is only provided internally through {_upgradeTo}. For an externally upgradeable proxy see
///      {TransparentUpgradeableProxy}.
abstract contract UpgradeableProxy is Proxy {

  /// @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
  ///      If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded
  ///      function call, and allows initializating the storage of the proxy like a Solidity constructor.
  constructor(address _logic) payable {
    assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1));
    _setImplementation(_logic);
  }

  /// @dev Emitted when the implementation is upgraded.
  event Upgraded(address indexed implementation);

  ///@dev Storage slot with the address of the current implementation.
  ///     This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
  ///     validated in the constructor.
  bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

  /// @dev Returns the current implementation address.
  function _implementation() internal view virtual override returns (address impl) {
    bytes32 slot = _IMPLEMENTATION_SLOT;
    // solhint-disable-next-line no-inline-assembly
    assembly {
      impl := sload(slot)
    }
  }

  /// @dev Upgrades the proxy to a new implementation.
  ///      Emits an {Upgraded} event.
  function _upgradeTo(address newImplementation) internal virtual {
    _setImplementation(newImplementation);
    emit Upgraded(newImplementation);
  }

  /// @dev Stores a new address in the EIP1967 implementation slot.
  function _setImplementation(address newImplementation) private {
    require(newImplementation.code.length != 0, "UpgradeableProxy: new implementation is not a contract");

    bytes32 slot = _IMPLEMENTATION_SLOT;

    // solhint-disable-next-line no-inline-assembly
    assembly {
      sstore(slot, newImplementation)
    }
  }
}

Contract Name:
EventLib

Contract Source Code:

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

/// @notice All errors of the app
interface IAppErrors {

  //region ERC20Errors
  /**
     * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
     * @param sender Address whose tokens are being transferred.
     * @param balance Current balance for the interacting account.
     * @param needed Minimum amount required to perform a transfer.
     */
  error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);

  /**
     * @dev Indicates a failure with the token `sender`. Used in transfers.
     * @param sender Address whose tokens are being transferred.
     */
  error ERC20InvalidSender(address sender);

  /**
     * @dev Indicates a failure with the token `receiver`. Used in transfers.
     * @param receiver Address to which tokens are being transferred.
     */
  error ERC20InvalidReceiver(address receiver);

  /**
     * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.
     * @param spender Address that may be allowed to operate on tokens without being their owner.
     * @param allowance Amount of tokens a `spender` is allowed to operate with.
     * @param needed Minimum amount required to perform a transfer.
     */
  error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);

  /**
     * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
     * @param approver Address initiating an approval operation.
     */
  error ERC20InvalidApprover(address approver);

  /**
   * @dev Indicates a failure with the `spender` to be approved. Used in approvals.
     * @param spender Address that may be allowed to operate on tokens without being their owner.
     */
  error ERC20InvalidSpender(address spender);

  //endregion ERC20Errors

  //region ERC721Errors

  /**
  * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20.
     * Used in balance queries.
     * @param owner Address of the current owner of a token.
     */
  error ERC721InvalidOwner(address owner);

  /**
   * @dev Indicates a `tokenId` whose `owner` is the zero address.
     * @param tokenId Identifier number of a token.
     */
  error ERC721NonexistentToken(uint256 tokenId);

  /**
   * @dev Indicates an error related to the ownership over a particular token. Used in transfers.
     * @param sender Address whose tokens are being transferred.
     * @param tokenId Identifier number of a token.
     * @param owner Address of the current owner of a token.
     */
  error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);

  /**
   * @dev Indicates a failure with the token `sender`. Used in transfers.
     * @param sender Address whose tokens are being transferred.
     */
  error ERC721InvalidSender(address sender);

  /**
   * @dev Indicates a failure with the token `receiver`. Used in transfers.
     * @param receiver Address to which tokens are being transferred.
     */
  error ERC721InvalidReceiver(address receiver);

  /**
   * @dev Indicates a failure with the `operator`’s approval. Used in transfers.
     * @param operator Address that may be allowed to operate on tokens without being their owner.
     * @param tokenId Identifier number of a token.
     */
  error ERC721InsufficientApproval(address operator, uint256 tokenId);

  /**
   * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
     * @param approver Address initiating an approval operation.
     */
  error ERC721InvalidApprover(address approver);

  /**
   * @dev Indicates a failure with the `operator` to be approved. Used in approvals.
     * @param operator Address that may be allowed to operate on tokens without being their owner.
     */
  error ERC721InvalidOperator(address operator);

  //endregion ERC721Errors

  error ZeroAddress();
  error ZeroValueNotAllowed();
  error ZeroToken();
  error LengthsMismatch();
  error NotEnoughBalance();
  error NotEnoughAllowance();
  error EmptyNameNotAllowed();
  error NotInitialized();
  error AlreadyInitialized();
  error ReentrancyGuardReentrantCall();
  error TooLongString();
  error AlreadyDeployed(address deployed);

  //region Restrictions
  error ErrorNotDeployer(address sender);
  error ErrorNotGoc();
  error NotGovernance(address sender);
  error ErrorOnlyEoa();
  error NotEOA(address sender);
  error ErrorForbidden(address sender);
  error AdminOnly();
  error ErrorNotItemController(address sender);
  error ErrorNotHeroController(address sender);
  error ErrorNotDungeonFactory(address sender);
  error ErrorNotObjectController(address sender);
  error ErrorNotStoryController();
  error ErrorNotAllowedSender();
  error MintNotAllowed();
  //endregion Restrictions

  //region PackingLib
  error TooHighValue(uint value);
  error IntValueOutOfRange(int value);
  error OutOfBounds(uint index, uint length);
  error UnexpectedValue(uint expected, uint actual);
  error WrongValue(uint newValue, uint actual);
  error IntOutOfRange(int value);
  error ZeroValue();
  /// @notice packCustomDataChange requires an input string with two zero bytes at the beginning
  ///         0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0000
  /// This error happens if these bytes are not zero
  error IncompatibleInputString();
  error IncorrectOtherItemTypeKind(uint8 kind);
  //endregion PackingLib

  //region Hero
  error ErrorHeroIsNotRegistered(address heroToken);
  error ErrorHeroIsDead(address heroToken, uint heroTokenId);
  error ErrorHeroNotInDungeon();
  error HeroInDungeon();
  error ErrorNotOwner(address token, uint tokenId);
  error Staked(address heroToken, uint heroId);
  error NameTaken();
  error TooBigName();
  error WrongSymbolsInTheName();
  error NoPayToken(address token, uint payTokenAmount);
  error AlreadyHaveReinforcement();
  /// @notice SIP-001 - Reinforcement requires 3 skills
  error ErrorReinforcementRequiresThreeSkills();
  error WrongTier(uint tier);
  error NotEnoughNgLevel(uint8 ngLevel);
  error NgpNotActive(address hero);
  error RebornNotAllowed();
  error AlreadyPrePaidHero();
  //endregion Hero

  //region Dungeon
  error ErrorDungeonIsFreeAlready();
  error ErrorNoEligibleDungeons();
  error ErrorDungeonBusy();
  error ErrorNoDungeonsForBiome(uint8 heroBiome);
  error ErrorDungeonCompleted();
  error ErrorAlreadyInDungeon();
  error NotEnoughTokens(uint balance, uint expectedBalance);
  error DungeonAlreadySpecific(uint16 dungNum);
  error DungeonAlreadySpecific2(uint16 dungNum);
  error WrongSpecificDungeon();
  //endregion Dungeon

  //region Items
  error ErrorItemNotEligibleForTheSlot(uint itemType, uint8 itemSlot);
  error ErrorItemSlotBusyHand(uint8 slot);
  error ErrorItemSlotBusy();
  error ErrorItemNotInSlot();
  error ErrorConsumableItemIsUsed(address item);
  error ErrorCannotRemoveItemFromMap();
  error ErrorCannotRemoveDataFromMap();
  error EquippedItemsExist();
  error ItemEquipped(address item, uint itemId);
  error ZeroItemMetaType();
  error NotZeroOtherItemMetaType();
  error ZeroLevel();
  error ItemTypeChanged();
  error ItemMetaTypeChanged();
  error UnknownItem(address item);
  error ErrorEquipForbidden();
  error EquipForbiddenInDungeon();
  error TakeOffForbiddenInDungeon();
  error Consumable(address item);
  error NotConsumable(address item);
  error Broken(address item);
  error ZeroLife();
  error RequirementsToItemAttributes();
  error NotEquipped(address item);
  error ZeroDurability();
  error ZeroAugmentation();
  error TooHighAgLevel(uint8 augmentationLevel);
  error UseForbiddenZeroPayToken();
  error IncorrectMinMaxAttributeRange(int32 min, int32 max);
  error SameIdsNotAllowed();
  error ZeroFragility();
  error OtherTypeItemNotRepairable();
  error NotOther();
  error DoubleItemUsageForbidden(uint itemIndex, address[] items);
  error ItemAlreadyUsedInSlot(address item, uint8 equippedSlot);
  error WrongWayToRegisterItem();
  error UnionItemNotFound(address item);
  error WrongListUnionItemTokens(address item, uint countTokens, uint requiredCountTokens);
  error UnknownUnionConfig(uint unionConfigId);
  error UserHasNoKeyPass(address user, address keyPassItem);
  error MaxValue(uint value);
  error UnexpectedOtherItem(address item);
  error NotExist();
  //endregion Items

  //region Stages
  error ErrorWrongStage(uint stage);
  error ErrorNotStages();
  //endregion Stages

  //region Level
  error ErrorWrongLevel(uint heroLevel);
  error ErrorLevelTooLow(uint heroLevel);
  error ErrorHeroLevelStartFrom1();
  error ErrorWrongLevelUpSum();
  error ErrorMaxLevel();
  //endregion Level

  //region Treasure
  error ErrorNotValidTreasureToken(address treasureToken);
  //endregion Treasure

  //region State
  error ErrorPaused();
  error ErrorNotReady();
  error ErrorNotObject1();
  error ErrorNotObject2();
  error ErrorNotCompleted();
  //endregion State

  //region Biome
  error ErrorNotBiome();
  error ErrorIncorrectBiome(uint biome);
  error TooHighBiome(uint biome);
  //endregion Biome

  //region Misc
  error ErrorWrongMultiplier(uint multiplier);
  error ErrorNotEnoughMana(uint32 mana, uint requiredMana);
  error ErrorExperienceMustNotDecrease();
  error ErrorNotEnoughExperience();
  error ErrorNotChances();
  error ErrorNotEligible(address heroToken, uint16 dungNum);
  error ErrorZeroKarmaNotAllowed();
  //endregion Misc

  //region GOC
  error GenObjectIdBiomeOverflow(uint8 biome);
  error GenObjectIdSubTypeOverflow(uint subType);
  error GenObjectIdIdOverflow(uint id);
  error UnknownObjectTypeGoc1(uint8 objectType);
  error UnknownObjectTypeGoc2(uint8 objectType);
  error UnknownObjectTypeGocLib1(uint8 objectType);
  error UnknownObjectTypeGocLib2(uint8 objectType);
  error UnknownObjectTypeForSubtype(uint8 objectSubType);
  error FightDelay();
  error ZeroChance();
  error TooHighChance(uint32 chance);
  error TooHighRandom(uint random);
  error EmptyObjects();
  error ObjectNotFound();
  error WrongGetObjectTypeInput();
  error WrongChances(uint32 chances, uint32 maxChances);
  //endregion GOC

  //region Story
  error PageNotRemovedError(uint pageId);
  error NotItem1();
  error NotItem2();
  error NotRandom(uint32 random);
  error NotHeroData();
  error NotGlobalData();
  error ZeroStoryIdRemoveStory();
  error ZeroStoryIdStoryAction();
  error ZeroStoryIdAction();
  error NotEnoughAmount(uint balance, uint requiredAmount);
  error NotAnswer();
  error AnswerStoryIdMismatch(uint16 storyId, uint16 storyIdFromAnswerHash);
  error AnswerPageIdMismatch(uint16 pageId, uint16 pageIdFromAnswerHash);
  //endregion Story

  //region FightLib
  error NotMagic();
  error NotAType(uint atype);
  //endregion FightLib

  //region MonsterLib
  error NotYourDebuffItem();
  error UnknownAttackType(uint attackType);
  error NotYourAttackItem();
  /// @notice The skill item cannot be used because it doesn't belong either to the hero or to the hero's helper
  error NotYourBuffItem();
  //endregion MonsterLib

  //region GameToken
  error ApproveToZeroAddress();
  error MintToZeroAddress();
  error TransferToZeroAddress();
  error TransferAmountExceedsBalance(uint balance, uint value);
  error InsufficientAllowance();
  error BurnAmountExceedsBalance();
  error NotMinter(address sender);
  //endregion GameToken

  //region NFT
  error TokenTransferNotAllowed();
  error IdOverflow(uint id);
  error NotExistToken(uint tokenId);
  error EquippedItemIsNotAllowedToTransfer(uint tokenId);
  //endregion NFT

  //region CalcLib
  error TooLowX(uint x);
  //endregion CalcLib

  //region Controller
  error NotFutureGovernance(address sender);
  //endregion Controller

  //region Oracle
  error OracleWrongInput();
  //endregion Oracle

  //region ReinforcementController
  error AlreadyStaked();
  error MaxFee(uint8 fee);
  error MinFee(uint8 fee);
  error StakeHeroNotStats();
  error NotStaked();
  error NoStakedHeroes();
  error GuildHelperNotAvailable(uint guildId, address helper, uint helperId);
  error HelperNotAvailableInGivenBiome();
  //endregion ReinforcementController

  //region SponsoredHero
  error InvalidHeroClass();
  error ZeroAmount();
  error InvalidProof();
  error NoHeroesAvailable();
  error AlreadyRegistered();
  //endregion SponsoredHero

  //region SacraRelay
  error SacraRelayNotOwner();
  error SacraRelayNotDelegator();
  error SacraRelayNotOperator();
  error SacraRelayInvalidChainId(uint callChainId, uint blockChainId);
  error SacraRelayInvalidNonce(uint callNonce, uint txNonce);
  error SacraRelayDeadline();
  error SacraRelayDelegationExpired();
  error SacraRelayNotAllowed();
  error SacraRelayInvalidSignature();
  /// @notice This error is generated when custom error is caught
  /// There is no info about custom error in SacraRelay
  /// but you can decode custom error by selector, see tests
  error SacraRelayNoErrorSelector(bytes4 selector, string tracingInfo);
  /// @notice This error is generated when custom error is caught
  /// There is no info about custom error in SacraRelay
  /// but you can decode custom error manually from {errorBytes} as following:
  /// if (keccak256(abi.encodeWithSignature("MyError()")) == keccak256(errorBytes)) { ... }
  error SacraRelayUnexpectedReturnData(bytes errorBytes, string tracingInfo);
  error SacraRelayCallToNotContract(address notContract, string tracingInfo);
  //endregion SacraRelay

  //region Misc
  error UnknownHeroClass(uint heroClass);
  error AbsDiff(int32 a, int32 b);
  //region Misc

  //region ------------------------ UserController
  error NoAvailableLootBox(address msgSender, uint lootBoxKind);
  error FameHallHeroAlreadyRegistered(uint8 openedNgLevel);

  //endregion ------------------------ UserController

  //region ------------------------ Guilds
  error AlreadyGuildMember();
  error NotGuildMember();
  error WrongGuild();
  error GuildActionForbidden(uint right);
  error GuildHasMaxSize(uint guildSize);
  error GuildHasMaxLevel(uint level);
  error TooLongUrl();
  error TooLongDescription();
  error CannotRemoveGuildOwnerFromNotEmptyGuild();
  error GuildControllerOnly();
  error GuildAlreadyHasShelter();
  error ShelterIsBusy();
  error ShelterIsNotRegistered();
  error ShelterIsNotOwnedByTheGuild();
  error ShelterIsInUse();
  error GuildHasNoShelter();
  error ShelterBidIsNotAllowedToBeUsed();
  error ShelterHasHeroesInside();
  error SecondGuildAdminIsNotAllowed();
  error NotEnoughGuildBankBalance(uint guildId);

  error GuildReinforcementCooldownPeriod();
  error NoStakedGuildHeroes();
  error NotStakedInGuild();
  error ShelterHasNotEnoughLevelForReinforcement();
  error NotBusyGuildHelper();

  error GuildRequestNotActive();
  error GuildRequestNotAvailable();
  error NotAdminCannotAddMemberWithNotZeroRights();
  //endregion ------------------------ Guilds

  //region ------------------------ Shelters
  error ErrorNotShelterController();
  error ErrorNotGuildController();
  error ShelterHasNotItem(uint shelterId, address item);
  error MaxNumberItemsSoldToday(uint numSoldItems, uint limit);
  error GuildHasNotEnoughPvpPoints(uint64 pointsAvailable, uint pointRequired);
  error FreeShelterItemsAreNotAllowed(uint shelterId, address item);
  error TooLowShelterLevel(uint8 shelterLevel, uint8 allowedShelterLevel);
  error NotEnoughPvpPointsCapacity(address user, uint usedPoints, uint pricePvpPoints, uint64 capactiy);
  error IncorrectShelterLevel(uint8 shelterLevel);
  //endregion ------------------------ Shelters

  //region ------------------------ Auction
  error WrongAuctionPosition();
  error AuctionPositionClosed();
  error AuctionBidOpened(uint positionId);
  error TooLowAmountToBid();
  error AuctionEnded();
  error TooLowAmountForNewBid();
  error AuctionSellerOnly();
  error AuctionBuyerOnly();
  error AuctionBidNotFound();
  error AuctionBidClosed();
  error OnlyShelterAuction();
  error CannotCloseLastBid();
  error AuctionNotEnded();
  error NotShelterAuction();
  error AuctionPositionOpened(uint positionId);
  error AuctionSellerCannotBid();
  error CannotApplyNotLastBid();
  error AuctionGuildWithShelterCannotBid();
  //endregion ------------------------ Auction

  //region ------------------------ Pawnshop
  error AuctionPositionNotSupported(uint positionId);
  error PositionNotSupported(uint positionId);
  error NotNftPositionNotSupported(uint positionId);
  error CallFailed(bytes callResultData);

  error PawnShopZeroOwner();
  error PawnShopZeroFeeRecipient();
  error PawnShopNotOwner();
  error PawnShopAlreadyAnnounced();
  error PawnShopTimeLock();
  error PawnShopWrongAddressValue();
  error PawnShopWrongUintValue();
  error PawnShopZeroAddress();
  error PawnShopTooHighValue();
  error PawnShopZeroAToken();
  error PawnShopZeroCToken();
  error PawnShopWrongAmounts();
  error PawnShopPosFeeForInstantDealForbidden();
  error PawnShopPosFeeAbsurdlyHigh();
  error PawnShopIncorrect();
  error PawnShopWrongId();
  error PawnShopNotBorrower();
  error PawnShopPositionClosed();
  error PawnShopPositionExecuted();
  error PawnShopWrongBidAmount();
  error PawnShopTooLowBid();
  error PawnShopNewBidTooLow();
  error PawnShopBidAlreadyExists();
  error PawnShopAuctionEnded();
  error PawnShopNotLender();
  error PawnShopTooEarlyToClaim();
  error PawnShopPositionNotExecuted();
  error PawnShopAlreadyClaimed();
  error PawnShopAuctionNotEnded();
  error PawnShopBidClosed();
  error PawnShopNoBids();
  error PawnShopAuctionBidNotFound();
  error PawnShopWrongBid();
  error PawnShopBidNotFound();

  //endregion ------------------------ Pawnshop
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "./IGOC.sol";
import "./IStatController.sol";
import "./IDungeonFactory.sol";
import "./IStoryController.sol";
import "./IFightCalculator.sol";

/// @notice All events of the app
interface IApplicationEvents {

  //region ------------------ StatController
  event HeroItemSlotChanged(
    address heroToken,
    uint heroTokenId,
    uint itemType,
    uint itemSlot,
    address itemToken,
    uint itemTokenId,
    bool equip,
    address caller
  );
  event CurrentStatsChanged(
    address heroToken,
    uint heroTokenId,
    IStatController.ChangeableStats change,
    bool increase,
    address caller
  );
  event BonusAttributesChanged(
    address heroToken,
    uint heroTokenId,
    bool add,
    bool temporally,
    address caller
  );
  event TemporallyAttributesCleared(address heroToken, uint heroTokenId, address caller);
  event NewHeroInited(address heroToken, uint heroTokenId, IStatController.ChangeableStats stats);
  event LevelUp(
    address heroToken,
    uint heroTokenId,
    uint heroClass,
    IStatController.CoreAttributes change
  );
  event ConsumableUsed(address heroToken, uint heroTokenId, address item);
  event RemoveConsumableUsage(address heroToken, uint heroTokenId, address item);
  event HeroCustomDataChanged(address token, uint tokenId, bytes32 index, uint value);
  event HeroCustomDataChangedNg(address token, uint tokenId, bytes32 index, uint value, uint8 ngLevel);
  event HeroCustomDataCleared(address token, uint tokenId);
  event GlobalCustomDataChanged(bytes32 index, uint value);
  //endregion ------------------ StatController

  //region ------------------ DungeonFactoryController
  event DungeonLaunched(
    uint16 dungeonLogicNum,
    uint64 dungeonId,
    address heroToken,
    uint heroTokenId,
    address treasuryToken,
    uint treasuryAmount
  );

  event BossCompleted(uint32 objectId, uint biome, address hero, uint heroId);
  event FreeDungeonAdded(uint8 biome, uint64 dungeonId);

  event ObjectOpened(uint64 dungId, address hero, uint id, uint32 objId, uint iteration, uint currentStage);
  event Clear(uint64 dungId);

  event DungeonLogicRegistered(uint16 dungLogicId, IDungeonFactory.DungeonGenerateInfo info);
  event DungeonLogicRemoved(uint16 dungLogicId);
  event DungeonSpecificLogicRegistered(uint16 dungLogicId, uint biome, uint heroCls);
  event DungeonSpecificLogicRemoved(uint16 dungLogicId, uint heroLvl, uint heroCls);
  event DungeonRegistered(uint16 dungLogicId, uint64 dungeonId);
  event DungeonRemoved(uint16 dungLogicId, uint64 dungeonId);
  event MinLevelForTreasuryChanged(address token, uint level);

  event ObjectAction(
    uint64 dungId,
    IGOC.ActionResult result,
    uint currentStage,
    address heroToken,
    uint heroTokenId,
    uint newStage
  );
  /// @notice On add the item to the dungeon
  event AddTreasuryItem(uint64 dungId, address itemAdr, uint itemId);
  event AddTreasuryToken(uint64 dungId, address token, uint amount);
  event ClaimToken(uint64 dungId, address token, uint amount);
  event ClaimItem(uint64 dungId, address token, uint id);

  event Entered(uint64 dungId, address hero, uint id);
  event DungeonCompleted(uint16 dungLogicNum, uint64 dungId, address hero, uint heroId);
  event Exit(uint64 dungId, bool claim);
  event ExitForcibly(uint64 dungId, address hero, uint heroId);
  event FreeDungeonRemoved(uint8 biome, uint64 dungeonId);
  event HeroCurrentDungeonChanged(address hero, uint heroId, uint64 dungeonId);
  //endregion ------------------ DungeonFactoryController

  //region ------------------ GameObjectController
  event EventRegistered(uint32 objectId, IGOC.EventRegInfo eventRegInfo);
  event StoryRegistered(uint32 objectId, uint16 storyId);
  event MonsterRegistered(uint32 objectId, IGOC.MonsterGenInfo monsterGenInfo);
  event ObjectRemoved(uint32 objectId);
  event ObjectResultEvent(
    uint64 dungeonId,
    uint32 objectId,
    IGOC.ObjectType objectType,
    address hero,
    uint heroId,
    uint8 stageId,
    uint iteration,
    bytes data,
    IGOC.ActionResult result,
    uint salt
  );
  //endregion ------------------ GameObjectController

  //region ------------------ StoryController
  event SetBurnItemsMeta(uint storyId, IStoryController.AnswerBurnRandomItemMeta meta);
  event SetNextObjRewriteMeta(uint storyId, IStoryController.NextObjRewriteMeta meta);
  event SetAnswersMeta(uint storyId, uint16[] answerPageIds, uint8[] answerHeroClasses, uint16[] answerIds);
  event SetAnswerNextPageMeta(uint storyId, IStoryController.AnswerNextPageMeta meta);
  event SetAnswerAttributeRequirements(uint storyId, IStoryController.AnswerAttributeRequirementsMeta meta);
  event SetAnswerItemRequirements(uint storyId, IStoryController.AnswerItemRequirementsMeta meta);
  event SetAnswerTokenRequirementsMeta(uint storyId, IStoryController.AnswerTokenRequirementsMeta meta);
  event SetAnswerAttributes(uint storyId, IStoryController.AnswerAttributesMeta meta);
  event SetAnswerHeroCustomDataRequirementMeta(uint storyId, IStoryController.AnswerCustomDataMeta meta);
  event SetAnswerGlobalCustomDataRequirementMeta(uint storyId, IStoryController.AnswerCustomDataMeta meta);
  event SetSuccessInfo(uint storyId, IStoryController.AnswerResultMeta meta);
  event SetFailInfo(uint storyId, IStoryController.AnswerResultMeta meta);
  event SetCustomDataResult(uint storyId, IStoryController.AnswerCustomDataResultMeta meta, IStoryController.CustomDataResult _type);
  event StoryCustomDataRequirements(uint storyId, bytes32 requiredCustomDataIndex, uint requiredCustomDataMinValue, uint requiredCustomDataMaxValue, bool requiredCustomDataIsHero);
  event StoryRequiredLevel(uint storyId, uint requiredLevel);
  event StoryFinalized(uint32 objectId, uint storyId);
  event StoryRemoved(uint32 objectId, uint storyId);

  event ItemBurned(
    address heroToken,
    uint heroTokenId,
    uint64 dungeonId,
    uint objectId,
    address nftToken,
    uint nftId,
    uint stageId,
    uint iteration
  );

  /// @notice Durability of the item was reduced to 0
  event ItemBroken(
    address heroToken,
    uint heroTokenId,
    uint64 dungeonId,
    uint objectId,
    address nftToken,
    uint nftId,
    uint stageId,
    uint iteration
  );


  event NotEquippedItemBurned(
    address heroToken,
    uint heroTokenId,
    uint64 dungeonId,
    uint storyId,
    address nftToken,
    uint nftId,
    uint stageId,
    uint iteration
  );

  event StoryChangeAttributes(
    uint32 objectId,
    address heroToken,
    uint heroTokenId,
    uint64 dungeonId,
    uint storyId,
    uint stageId,
    uint iteration,
    int32[] attributes
  );
  //endregion ------------------ StoryController

  //region ------------------------ HeroController
  event HeroRegistered(address hero, uint8 heroClass, address payToken, uint payAmount);
  event HeroCreatedNgp(address hero, uint heroId, string name, address owner, string refCode, uint8 tier, uint8 ngLevel);
  event BiomeChanged(address hero, uint heroId, uint8 biome);
  event LevelUp(address hero, uint heroId, address owner, IStatController.CoreAttributes change);
  event ReinforcementAsked(address hero, uint heroId, address helpHeroToken, uint helpHeroId);
  event GuildReinforcementAsked(address hero, uint heroId, address helpHeroToken, uint helpHeroId);
  event OtherItemGuildReinforcement(address item, uint itemId, address hero, uint heroId, address helpHeroToken, uint helpHeroId);
  event ReinforcementReleased(address hero, uint heroId, address helperToken, uint helperId);
  event GuildReinforcementReleased(address hero, uint heroId, address helperToken, uint helperId);
  event Killed(address hero, uint heroId, address killer, bytes32[] dropItems, uint dropTokenAmount);
  event Reborn(address hero, uint heroId, uint8 newNgLevel);
  event BossKilled(address account, address hero, uint heroId, uint8 biome, uint8 newNgLevel, bool reborn, uint rewardAmount);
  event TierSetup(uint8 tier, address hero, uint72 payAmount, uint8[] slots, address[][] items);
  //endregion ------------------------ HeroController

  //region ------------------------ FightLib
  event FightResultProcessed(
    address sender,
    IFightCalculator.FightInfoInternal result,
    IFightCalculator.FightCall callData,
    uint iteration
  );
  //endregion ------------------------ FightLib

  //region ------------------------ Oracle
  event Random(uint number, uint max);
  //endregion ------------------------ Oracle

  //region ------------------------ Controller
  event OfferGovernance(address newGov);
  event GovernanceAccepted(address gov);
  event StatControllerChanged(address value);
  event StoryControllerChanged(address value);
  event GameObjectControllerChanged(address value);
  event ReinforcementControllerChanged(address value);
  event OracleChanged(address value);
  event TreasuryChanged(address value);
  event ItemControllerChanged(address value);
  event HeroControllerChanged(address value);
  event GameTokenChanged(address value);
  event DungeonFactoryChanged(address value);
  event ProxyUpdated(address proxy, address logic);
  event Claimed(address token, uint amount);
  event TokenStatusChanged(address token, bool status);
  event UserControllerChanged(address value);
  event GuildControllerChanged(address value);
  event GameTokenPriceChanged(uint value);
  event RewardsPoolChanged(address value);
  event Process(address token, uint amount, address from, uint toBurn, uint toTreasury, uint toGov);
  //endregion ------------------------ Controller


  //region ------------------------ ReinforcementController
  event HeroStaked(address heroToken, uint heroId, uint biome, uint score);
  event HeroStakedV2(address heroToken, uint heroId, uint biome, uint rewardAmount);
  event HeroWithdraw(address heroToken, uint heroId);
  event HeroAsk(address heroToken, uint heroId);
  event HeroAskV2(address heroToken, uint heroId, uint hitsLast24h, uint fixedFee, uint helperRewardAmount);
  event TokenRewardRegistered(address heroToken, uint heroId, address token, uint amountAdded, uint totalAmount);
  event GuildTokenRewardRegistered(address heroToken, uint heroId, address token, uint amountAdded, uint guildId);
  event NftRewardRegistered(address heroToken, uint heroId, address token, uint id);
  event GuildNftRewardRegistered(address heroToken, uint heroId, address token, uint id, uint guildId);
  event ToHelperRatioChanged(uint value);
  event ClaimedToken(address heroToken, uint heroId, address token, uint amount, address recipient);
  event ClaimedItem(address heroToken, uint heroId, address item, uint itemId, address recipient);
  event MinLevelChanged(uint8 value);
  event MinLifeChancesChanged(uint value);
  //endregion ------------------------ ReinforcementController

  //region ------------------------ Treasury, reward pool
  event AssetsSentToDungeon(address dungeon, address token, uint amount);
  event RewardSentToUser(address receiver, address token, uint rewardAmount);
  event NotEnoughReward(address receiver, address token, uint rewardAmountToPay);
  event BaseAmountChanged(uint oldValue, uint newValue);
  //endregion ------------------------ Treasury, reward pool

  //region ------------------------ EventLib
  event EventResult(uint64 dungeonId, address heroToken, uint heroTokenId, uint8 stageId, IStatController.ActionInternalInfo gen, uint iteration);
  //endregion ------------------------ EventLib

  //region ------------------------ Item controller and helper contracts
  event ItemRegistered(address item, IItemController.RegisterItemParams info);
  event OtherItemRegistered(address item, IItemController.ItemMeta meta, bytes packedItemMetaData);
  event ItemRemoved(address item);
  event OtherItemRemoved(address item);
  event NewItemMinted(address item, uint itemId, IItemController.MintInfo info);
  event Equipped(address item, uint itemId, address heroToken, uint heroTokenId, uint8 itemSlot);
  event TakenOff(address item, uint itemId, address heroToken, uint heroTokenId, uint8 itemSlot, address destination);
  event ItemRepaired(address item, uint itemId, uint consumedItemId, uint16 baseDurability);
  event FailedToRepairItem(address item, uint itemId, uint consumedItemId, uint16 itemDurability);
  event Augmented(address item, uint itemId, uint consumedItemId, uint8 augLevel, IItemController.AugmentInfo info);
  event NotAugmented(address item, uint itemId, uint consumedItemId, uint8 augLevel);
  event ReduceDurability(address item, uint itemId, uint newDurability);
  event Used(address item, uint tokenId, address heroToken, uint heroTokenId);
  event Destroyed(address item, uint itemId);
  event FragilityReduced(address item, uint itemId, address consumedItem, uint consumedItemId, uint fragility);
  event ItemControllerHelper(address helper);
  event SetUnionConfig(uint configId, address[] items, uint[] count, address itemToMint);
  event RemoveUnionConfig(uint configId);
  event SetUnionKeyPass(address keyPassItem);
  event CombineItems(address msgSender, uint configId, address[] items, uint[][] itemIds, address mintedItem, uint mintedItemId);
  //endregion ------------------------ Item controller and helper contracts

  //region ------------------------ NFT and GameToken (only custom events, not ERC20/721 standards)
  event ChangePauseStatus(bool value);
  event MinterChanged(address value);

  event UniqueUriChanged(uint id, string uri);
  event BaseUriChanged(string uri);

  event HeroMinted(uint heroId);
  event HeroBurned(uint heroId);
  event HeroUriByStatusChanged(string uri, uint statusLvl);

  event ItemMinted(uint tokenId);
  event ItemBurned(uint tokenId);
  event UriByRarityChanged(string uri, uint rarity);
  event SponsoredHeroCreated(address msgSender, address heroAddress, uint heroId, string heroName);
  //endregion ------------------------ NFT and GameToken (only custom events, not ERC20/721 standards)

  //region ------------------------ User controller
  event SetUserName(address user, string name);
  event SetUserAvatar(address user, string avatar);
  event LootBoxOpened(address user, uint lootBoxKind, address[] itemTokens, uint[] itemTokenIds);
  event LootBoxConfigChanged(uint lootBoxKind, address[] mintItems, uint32[] mintItemsChances, uint maxDropItems);
  event SetFeeRenaming(uint feeRenaming);
  event ActivityCompleted(address user, bool daily, bool weekly);
  event FameHallHeroRegistered(address hero, uint heroId, address heroOwner, uint8 openedNgLevel);
  //endregion ------------------------ User controller

  //region ------------------------ Guild

  event GuildCreated(address owner, uint guildId, string name, string urlLogo);
  event AddToGuild(uint guildId, address newUser);
  event ChangeGuildRights(uint guildId, address user, uint rights);
  event RemoveFromGuild(uint guildId, address user);
  event GuildDeleted(uint guildId);
  event GuildLevelUp(uint guildId, uint8 newLevel);
  event GuildRename(uint guildId, string newName);
  event GuildLogoChanged(uint guildId, string newLogoUrl);
  event GuildDescriptionChanged(uint guildId, string newDescription);
  event SetGuildRelation(uint guildId1, uint guildId2, bool peace);
  event TransferFromGuildBank(address user, address token, uint amount, address recipient);
  event TransferNftFromGuildBank(address user, address[] nfts, uint[] tokenIds, address recipient);
  event GuildBankDeployed(uint guildId, address guildBank);

  event SetToHelperRatio(uint guildId, uint8 value, address user);
  event TopUpGuildBank(address msgSender, uint guildId, address guildBank, uint amount);

  event GuildRequestRegistered(address msgSender, uint guildId, string userMessage, uint depositAmount);
  event GuildRequestStatusChanged(address msgSender, uint guildRequestId, uint8 newStatus, address user);
  event SetToHelperRatio(uint guildId, address msgSender, uint8 toHelperRatio);
  event SetGuildRequestDepositAmount(uint guildId, address msgSender, uint amount);
  event SetGuildBaseFee(uint fee);
  event SetPvpPointsCapacity(address msgSender, uint64 capacityPvpPoints, address[] users);
  event SetShelterController(address shelterController);
  event SetShelterAuction(address shelterAuction);
  event PayForBidFromGuildBank(uint guildId, uint amount, uint bid);
  //endregion ------------------------ Guild

  //region ------------------------ Guild shelter
  event RegisterShelter(uint sheleterId, uint price);
  event SetShelterItems(
    uint shelterId,
    address[] items,
    uint64[] pricesInPvpPoints,
    uint128[] pricesInGameTokens,
    uint16[] maxItemsPerDayThresholds
  );
  event RemoveShelterItems(uint shelterId, address[] items);
  event BuyShelter(uint guidlId, uint shelterId);
  event LeaveShelter(uint guildId, uint shelterId);
  event NewShelterBid(uint shelterId, uint buyerGuildId, uint amount);
  event RevokeShelterBid(uint shelterId);
  event UseShelterBid(uint shelterId, uint sellerGuildId, uint buyerGuidId, uint amount);
  event PurchaseShelterItem(address msgSender, address item, uint numSoldItems, uint priceInPvpPoints, uint priceInGameToken);
  event ChangeShelterOwner(uint shelterId, uint fromGuildId, uint toGuildId);
  event RestInShelter(address msgSender, address heroToken, uint heroTokenId);
  //endregion ------------------------ Guild shelter

  //region ------------------------ Guild reinforcement
  event GuildHeroStaked(address heroToken, uint heroId, uint guildId);
  event GuildHeroWithdrawn(address heroToken, uint heroId, uint guildId);
  event GuildHeroAsked(address heroToken, uint heroId, uint guildId, address user);

  /// @param user Address can be 0 if heroId was already burnt at the moment of reinforcement releasing
  event GuildHeroReleased(address heroToken, uint heroId, uint guildId, address user);
  //endregion ------------------------ Guild reinforcement

  //region ------------------------ Guild auction
  event AuctionPositionOpened(uint positionId, uint shelterId, uint sellerGuildId, address msgSender, uint minAuctionPrice);
  event AuctionPositionClosed(uint positionId, address msgSender);
  event AuctionBidOpened(uint bidId, uint positionId, uint amount, address msgSender);
  //endregion ------------------------ Guild auction

  //region ------------------------ Guild bank
  event GuildBankTransfer(address token, address recipient, uint amount);
  event GuildBankTransferNft(address to, address nft, uint tokenId);
  event GuildBankTransferNftMulti(address to, address[] nfts, uint[] tokenIds);
  //endregion ------------------------ Guild bank

  //region ------------------------ Pawnshop
  event PawnShopRouterDeployed(address pawnShop, address gameToken, address routerOwner, address deployed);
  event PawnShopRouterTransfer(address token, uint amount, address receiver);
  event PawnShopRouterBulkSell(address[] nfts, uint[] nftIds, uint[] prices, address nftOwner, uint[] positionIds);
  event PawnShopRouterClosePositions(uint[] positionIds, address receiver);
  event PawnShopRouterBulkBuy(uint[] positionIds, address receiver);

  //endregion ------------------------ Pawnshop
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IController {

  function governance() external view returns (address);

  function statController() external view returns (address);

  function storyController() external view returns (address);

  function gameObjectController() external view returns (address);

  function reinforcementController() external view returns (address);

  function oracle() external view returns (address);

  function treasury() external view returns (address);

  function itemController() external view returns (address);

  function heroController() external view returns (address);

  function dungeonFactory() external view returns (address);

  function gameToken() external view returns (address);

  function validTreasuryTokens(address token) external view returns (bool);

  function isDeployer(address adr) external view returns (bool);

  function onPause() external view returns (bool);

  function userController() external view returns (address);

  function guildController() external view returns (address);

  function rewardsPool() external view returns (address);

  function gameTokenPrice() external view returns (uint);

  function process(address token, uint amount, address from) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "../openzeppelin/EnumerableSet.sol";
import "../openzeppelin/EnumerableMap.sol";

interface IDungeonFactory {

  /// @custom:storage-location erc7201:dungeon.factory.main
  struct MainState {
    /// @dev biome => dungeonLaunchedId
    mapping(uint => EnumerableSet.UintSet) freeDungeons;
    /// @dev hero + heroId + biome (packMapObject) -> completed
    mapping(bytes32 => bool) bossCompleted;
    /// @dev hero + heroId + dungNum (packDungeonKey) -> completed
    mapping(bytes32 => bool) specificDungeonCompleted;
    /// @notice Max biome completed by the hero
    /// @dev hero + heroId (nftPacked) -> max biome completed
    mapping(bytes32 => uint8) maxBiomeCompleted;
    /// @notice which dungeon the hero is currently in
    /// @dev hero+id => current DungeonId
    mapping(bytes32 => uint64) heroCurrentDungeon;

    // ---

    /// @notice Specific dungeon for the given pair of hero level + hero class
    ///         ALl specific dungeons are listed also in allSpecificDungeons
    /// @dev packUint8Array(specReqBiome, specReqHeroClass) => dungNum
    mapping(bytes32 => uint16) dungeonSpecific;
    /// @dev contains all specific dungNum for easy management
    EnumerableSet.UintSet allSpecificDungeons;
    /// @dev biome => dungNum
    mapping(uint8 => EnumerableSet.UintSet) dungeonsLogicByBiome;

    // ---

    /// @dev max available biome. auto-increment with new dung deploy
    uint8 maxBiome;

    /// @notice Address of treasure token => min hero level required
    /// @dev manual threshold for treasury
    mapping(address => uint) minLevelForTreasury;

    /// @notice Contains arrays for SKILL_1, SKILL_2, SKILL_3 with 0 or 1
    /// i.e. [0, 1, 0] means that durability of SKILL_2 should be reduced
    /// @dev hero + heroId => uint8[] array where idx = slotNum
    mapping(bytes32 => bytes32) skillSlotsForDurabilityReduction;

    /// @notice Counter of dungeons, it's incremented on launch of a new dungeon
    uint64 dungeonCounter;

    /// @dev dungNum = init attributes
    mapping(uint16 => DungeonAttributes) dungeonAttributes;
    /// @dev dungeonId => status
    mapping(uint64 => DungeonStatus) dungeonStatuses;
  }

  struct ObjectGenerateInfo {
    /// @notice List of chamber types for each unique object
    /// @dev uint8 types, packed using PackingLib.packUint8Array
    bytes32[] objTypesByStages;
    /// @notice List of chances for each chamber type
    /// @dev uint64 chances
    uint32[][] objChancesByStages;
  }

  struct DungeonGenerateInfo {
    /// @notice List of chamber types for each unique object
    uint8[][] objTypesByStages;
    /// @notice List of chances for each chamber type
    uint32[][] objChancesByStages;

    uint32[] uniqObjects;

    uint8 minLevel;
    uint8 maxLevel;

    bytes32[] requiredCustomDataIndex;
    uint64[] requiredCustomDataMinValue;
    uint64[] requiredCustomDataMaxValue;
    bool[] requiredCustomDataIsHero;
  }

  /// @notice Attributes of the given dungeon logic
  struct DungeonAttributes {
    /// @notice Total number of stages that should be passed to complete the dungeon
    uint8 stages;
    uint8 biome;

    /// @notice Default list of objects that should be passed in the dungeon
    uint32[] uniqObjects;

    /// @dev min+max (packUint8Array)
    bytes32 minMaxLevel;

    bytes32[] requiredCustomDataIndex;
    /// @notice Packed DungeonGenerateInfo.requiredCustomData: MinValue, MaxValue, IsHero
    /// @dev min+max+isHero(packStoryCustomDataRequirements)
    bytes32[] requiredCustomDataValue;

    ObjectGenerateInfo info;
  }

  /// @notice Current status of the given dungeon
  struct DungeonStatus {
    uint64 dungeonId;
    /// @notice Dungeon logic id
    uint16 dungNum;

    /// @notice True if the dungeon is completed by the hero
    bool isCompleted;

    /// @notice Hero in the dungeon or 0
    address heroToken;
    uint heroTokenId;
    /// @notice Current object that should be passed by the hero. 0 - new object is not opened
    uint32 currentObject;
    /// @notice Current stage in the dungeon that should be passed by the hero.
    uint8 currentStage;

    EnumerableMap.AddressToUintMap treasuryTokens;
    /// @notice All items that were minted on result of made actions
    bytes32[] treasuryItems;

    /// @notice Total number of stages that should be passed to complete the dungeon
    /// This value can be bigger than length of uniqObjects
    uint8 stages;
    /// @notice List of objects to be passed in the stage. The list can be dynamically changed during passing the stages
    uint32[] uniqObjects;
  }

  ////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////

  function launchForNewHero(address heroToken, uint heroTokenId, address owner) external returns (uint64 dungeonId);

  function maxBiomeCompleted(address heroToken, uint heroTokenId) external view returns (uint8);

  function currentDungeon(address heroToken, uint heroTokenId) external view returns (uint64);

  function skillSlotsForDurabilityReduction(address heroToken, uint heroTokenId) external view returns (uint8[] memory result);

  function setBossCompleted(uint32 objectId, address heroToken, uint heroTokenId, uint8 heroBiome) external;

  /// @notice Hero exists current dungeon forcibly same as when dying but without loosing life chance
  function exitForcibly(address heroToken, uint heroTokenId, address msgSender) external;

  function maxAvailableBiome() external view returns (uint8);

  function reborn(address hero, uint heroId) external;
}

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
  /**
   * @dev Returns true if this contract implements the interface defined by
   * `interfaceId`. See the corresponding
   * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
   * to learn more about how these ids are created.
   *
   * This function call must use less than 30 000 gas.
   */
  function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
  /**
   * @dev Returns the amount of tokens in existence.
   */
  function totalSupply() external view returns (uint256);

  /**
   * @dev Returns the amount of tokens owned by `account`.
   */
  function balanceOf(address account) external view returns (uint256);

  /**
   * @dev Moves `amount` tokens from the caller's account to `recipient`.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  function transfer(address recipient, uint256 amount) external returns (bool);

  /**
   * @dev Returns the remaining number of tokens that `spender` will be
   * allowed to spend on behalf of `owner` through {transferFrom}. This is
   * zero by default.
   *
   * This value changes when {approve} or {transferFrom} are called.
   */
  function allowance(address owner, address spender) external view returns (uint256);

  /**
   * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * IMPORTANT: Beware that changing an allowance with this method brings the risk
   * that someone may use both the old and the new allowance by unfortunate
   * transaction ordering. One possible solution to mitigate this race
   * condition is to first reduce the spender's allowance to 0 and set the
   * desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   *
   * Emits an {Approval} event.
   */
  function approve(address spender, uint256 amount) external returns (bool);

  /**
   * @dev Moves `amount` tokens from `sender` to `recipient` using the
   * allowance mechanism. `amount` is then deducted from the caller's
   * allowance.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  function transferFrom(
    address sender,
    address recipient,
    uint256 amount
  ) external returns (bool);

  /**
   * @dev Emitted when `value` tokens are moved from one account (`from`) to
   * another (`to`).
   *
   * Note that `value` may be zero.
   */
  event Transfer(address indexed from, address indexed to, uint256 value);

  /**
   * @dev Emitted when the allowance of a `spender` for an `owner` is set by
   * a call to {approve}. `value` is the new allowance.
   */
  event Approval(address indexed owner, address indexed spender, uint256 value);
}

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC165.sol";

/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
  /**
   * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
   */
  event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

  /**
   * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
   */
  event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

  /**
   * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
   */
  event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

  /**
   * @dev Returns the number of tokens in ``owner``'s account.
   */
  function balanceOf(address owner) external view returns (uint256 balance);

  /**
   * @dev Returns the owner of the `tokenId` token.
   *
   * Requirements:
   *
   * - `tokenId` must exist.
   */
  function ownerOf(uint256 tokenId) external view returns (address owner);

  /**
   * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
   * are aware of the ERC721 protocol to prevent tokens from being forever locked.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must exist and be owned by `from`.
   * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.
   * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
   *
   * Emits a {Transfer} event.
   */
  function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId
  ) external;

  /**
   * @dev Transfers `tokenId` token from `from` to `to`.
   *
   * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must be owned by `from`.
   * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
   *
   * Emits a {Transfer} event.
   */
  function transferFrom(
    address from,
    address to,
    uint256 tokenId
  ) external;

  /**
   * @dev Gives permission to `to` to transfer `tokenId` token to another account.
   * The approval is cleared when the token is transferred.
   *
   * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
   *
   * Requirements:
   *
   * - The caller must own the token or be an approved operator.
   * - `tokenId` must exist.
   *
   * Emits an {Approval} event.
   */
  function approve(address to, uint256 tokenId) external;

  /**
   * @dev Returns the account approved for `tokenId` token.
   *
   * Requirements:
   *
   * - `tokenId` must exist.
   */
  function getApproved(uint256 tokenId) external view returns (address operator);

  /**
   * @dev Approve or remove `operator` as an operator for the caller.
   * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
   *
   * Requirements:
   *
   * - The `operator` cannot be the caller.
   *
   * Emits an {ApprovalForAll} event.
   */
  function setApprovalForAll(address operator, bool _approved) external;

  /**
   * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
   *
   * See {setApprovalForAll}
   */
  function isApprovedForAll(address owner, address operator) external view returns (bool);

  /**
   * @dev Safely transfers `tokenId` token from `from` to `to`.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must exist and be owned by `from`.
   * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
   * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
   *
   * Emits a {Transfer} event.
   */
  function safeTransferFrom(
    address from,
    address to,
    uint256 tokenId,
    bytes calldata data
  ) external;
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;

import "./IERC721.sol";

/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
  /**
   * @dev Returns the total amount of tokens stored by the contract.
   */
  function totalSupply() external view returns (uint256);

  /**
   * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
   * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
   */
  function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

  /**
   * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
   * Use along with {totalSupply} to enumerate all tokens.
   */
  function tokenByIndex(uint256 index) external view returns (uint256);
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "./IStatController.sol";
import "./IItemController.sol";

interface IFightCalculator {

  enum AttackType {
    UNKNOWN, // 0
    MELEE, // 1
    MAGIC, // 2
    SLOT_3,
    SLOT_4,
    SLOT_5,
    SLOT_6,
    SLOT_7,
    SLOT_8,
    SLOT_9,
    SLOT_10
  }

  /// @notice Attacker info: suitable both for hero and monsters
  struct AttackInfo {
    /// @notice Type of the attack
    /// by default, if attack token presents, it's magic attack and not-magic otherwise
    /// but this logic can become more complicated after introducing new attack types
    AttackType attackType;
    /// @notice NFT selected by hero for attack, it should be equip on.
    /// If attacker is a monster, this is a special case (stub NFT with zero ID is used)
    address attackToken;
    uint attackTokenId;
    address[] skillTokens;
    uint[] skillTokenIds;
  }

  struct FighterInfo {
    int32[] fighterAttributes;
    IStatController.ChangeableStats fighterStats;
    AttackType attackType;
    address attackToken;
    uint attackTokenId;
    uint race;
  }

  struct Statuses {
    bool stun;
    bool burn;
    bool freeze;
    bool confuse;
    bool curse;
    bool poison;
    bool gotCriticalHit;
    bool missed;
    bool hitBlocked;
  }

  struct FightResult {
    int32 healthA;
    int32 healthB;
    int32 manaConsumedA;
    int32 manaConsumedB;
  }

  struct FightCall {
    FighterInfo fighterA;
    FighterInfo fighterB;
    uint64 dungeonId;
    uint32 objectId;
    address heroAdr;
    uint heroId;
    uint8 stageId;
    uint iteration;
    uint8 turn;
  }

  struct SkillSlots {
    bool slot1;
    bool slot2;
    bool slot3;
  }

  //region ------------------------ FightLib-internal (FightInfoInternal is required by IApplicationEvents..)
  struct FightInfoInternal {
    Fighter fighterA;
    Fighter fighterB;
  }

  struct Fighter {
    IFightCalculator.FighterInfo info;
    IItemController.AttackInfo magicAttack;
    int32 health;
    int32 manaConsumed;
    int32 damage;
    int32 damagePoison;
    int32 damageReflect;
    IFightCalculator.Statuses statuses;
  }
  //endregion ------------------------ FightLib-internal

  function fight(FightCall memory callData) external returns (FightResult memory);
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "./IERC20.sol";

interface IGameToken is IERC20 {

  function minter() external view returns (address);

  function mint(address account, uint amount) external returns (bool);

  function burn(uint amount) external returns (bool);

  function setMinter(address minter_) external;

  function pause(bool value) external;

}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "../openzeppelin/EnumerableSet.sol";
import "./IController.sol";

interface IGOC {

  enum ObjectType {
    UNKNOWN, // 0
    EVENT, // 1
    MONSTER, // 2
    STORY, // 3
    END_SLOT
  }

  enum ObjectSubType {
    UNKNOWN_0, // 0
    ENEMY_NPC_1, // 1
    ENEMY_NPC_SUPER_RARE_2, // 2
    BOSS_3, // 3
    SHRINE_4, // 4
    CHEST_5, // 5
    STORY_6, // 6
    STORY_UNIQUE_7, // 7
    SHRINE_UNIQUE_8, // 8
    CHEST_UNIQUE_9, // 9
    ENEMY_NPC_UNIQUE_10, // 10
    STORY_ON_ROAD_11, // 11
    STORY_UNDERGROUND_12, // 12
    STORY_NIGHT_CAMP_13, // 13
    STORY_MOUNTAIN_14, // 14
    STORY_WATER_15, // 15
    STORY_CASTLE_16, // 16
    STORY_HELL_17, // 17
    STORY_SPACE_18, // 18
    STORY_WOOD_19, // 19
    STORY_CATACOMBS_20, // 20
    STORY_BAD_HOUSE_21, // 21
    STORY_GOOD_TOWN_22, // 22
    STORY_BAD_TOWN_23, // 23
    STORY_BANDIT_CAMP_24, // 24
    STORY_BEAST_LAIR_25, // 25
    STORY_PRISON_26, // 26
    STORY_SWAMP_27, // 27
    STORY_INSIDE_28, // 28
    STORY_OUTSIDE_29, // 29
    STORY_INSIDE_RARE_30,
    STORY_OUTSIDE_RARE_31,
    ENEMY_NPC_INSIDE_32,
    ENEMY_NPC_INSIDE_RARE_33,
    ENEMY_NPC_OUTSIDE_34,
    ENEMY_NPC_OUTSIDE_RARE_35,
    END_SLOT
  }

  /// @custom:storage-location erc7201:game.object.controller.main
  struct MainState {

    /// @dev objId = biome(00) type(00) id(0000) => biome(uint8) + objType(uint8)
    /// Id is id of the event, story or monster.
    mapping(uint32 => bytes32) objectMeta;

    /// @dev biome(uint8) + objType(uint8) => set of object id
    mapping(bytes32 => EnumerableSet.UintSet) objectIds;

    /// @dev heroAdr180 + heroId64 + cType8 + biome8 => set of already played objects. Should be cleared periodically
    mapping(bytes32 => EnumerableSet.UintSet) playedObjects;

    /// @dev HeroAdr(160) + heroId(uint64) + objId(uint32) => iteration count. It needs for properly emit events for every new entrance.
    mapping(bytes32 => uint) iterations;

    /// @dev objId(uint32) => EventInfo
    mapping(uint32 => EventInfo) eventInfos;

    /// @dev objId(uint32) => storyId
    mapping(uint32 => uint16) storyIds;

    /// @dev objId(uint32) => MonsterInfo
    mapping(uint32 => MonsterInfo) monsterInfos;

    /// @dev hero+id => last fight action timestamp
    mapping(bytes32 => uint) lastHeroFightTs;

    /// @dev delay for user actions in fight (suppose to prevent bot actions)
    uint fightDelay;
  }

  struct ActionResult {
    bool kill;
    bool completed;
    address heroToken;
    address[] mintItems;
    int32 heal;
    int32 manaRegen;
    int32 lifeChancesRecovered;
    int32 damage;
    int32 manaConsumed;
    uint32 objectId;
    uint32 experience;
    uint heroTokenId;
    uint iteration;
    uint32[] rewriteNextObject;
  }

  struct EventInfo {
    /// @dev chance to use good or bad attributes/stats
    uint32 goodChance;

    /// @dev toBytes32ArrayWithIds
    bytes32[] goodAttributes;
    bytes32[] badAttributes;

    /// @dev experience(uint32) + heal(int32) + manaRegen(int32) + lifeChancesRecovered(int32) + damage(int32) + manaConsume(int32) packStatsChange
    bytes32 statsChange;

    /// @dev item+chance packItemMintInfo
    bytes32[] mintItems;
  }

  struct MonsterInfo {
    /// @dev toBytes32ArrayWithIds
    bytes32[] attributes;
    /// @dev level(uint8) + race(uint8) + experience(uint32) + maxDropItems(uint8) packMonsterStats
    bytes32 stats;
    /// @dev attackToken(160) + attackTokenId(uint64) + attackType(uint8) packAttackInfo
    bytes32 attackInfo;

    /// @dev item+chance packItemMintInfo
    bytes32[] mintItems;

    /// @dev heroAdr(160) + heroId(uint64) => iteration => GeneratedMonster packed
    mapping(bytes32 => mapping(uint => bytes32)) _generatedMonsters;
  }

  struct MultiplierInfo {
    uint8 biome;
    /// @notice NG_LEVEL of the hero who is going to fight with the given monster
    /// Use type(uint8).max for !NG+
    uint8 heroNgLevel;
  }

  struct GeneratedMonster {
    bool generated;
    uint8 turnCounter;
    int32 hp;
    uint32 amplifier;
  }

  struct MonsterGenInfo {
    uint16 monsterId;
    uint8 biome;
    ObjectSubType subType;

    uint8[] attributeIds;
    int32[] attributeValues;

    uint8 level;
    uint8 race;
    uint32 experience;
    uint8 maxDropItems;

    address attackToken;
    uint64 attackTokenId;
    uint8 attackType;

    address[] mintItems;
    uint32[] mintItemsChances;
  }

  struct ActionContext {
    address sender;
    address heroToken;
    IController controller;
    uint8 biome;
    uint8 objectSubType;
    uint8 stageId;
    uint8 heroNgLevel;
    uint32 objectId;
    uint64 dungeonId;
    uint heroTokenId;
    uint salt;
    uint iteration;
    bytes data;
  }

  struct EventRegInfo {
    uint8 biome;
    uint16 eventId;
    ObjectSubType subType;

    uint32 goodChance;

    AttributeGenerateInfo goodAttributes;
    AttributeGenerateInfo badAttributes;

    uint32 experience;
    int32 heal;
    int32 manaRegen;
    int32 lifeChancesRecovered;
    int32 damage;
    int32 manaConsumed;

    address[] mintItems;
    uint32[] mintItemsChances;
  }

  struct AttributeGenerateInfo {
    uint8[] ids;
    int32[] values;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////

  /// @dev represent object registration if non zero values
  function getObjectMeta(uint32 objectId) external view returns (uint8 biome, uint8 objectSubType);

  function isBattleObject(uint32 objectId) external view returns (bool);

  function getRandomObject(
    uint8[] memory cTypes,
    uint32[] memory chances,
    uint8 biomeLevel,
    address heroToken,
    uint heroTokenId
  ) external returns (uint32 objectId);

  function open(address heroToken, uint heroTokenId, uint32 objectId) external returns (uint iteration);

  function action(
    address sender,
    uint64 dungeonId,
    uint32 objectId,
    address heroToken,
    uint heroTokenId,
    uint8 stageId,
    bytes memory data
  ) external returns (ActionResult memory);

}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IAppErrors.sol";
import "../interfaces/IGuildController.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/IERC721.sol";

interface IGuildBank {
  function transfer(address token, address recipient, uint amount) external;

  function approve(address token, address spender, uint256 amount) external returns (bool);

  function transferNft(address to, address nft, uint256 tokenId) external;

  function transferNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external;

  function approveNft(address to, address nft, uint256 tokenId) external;

  function approveNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external;
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../openzeppelin/EnumerableSet.sol";

interface IGuildController {
  enum GuildRightBits {
    ADMIN_0,
    RENAME_1,
    CHANGE_LOGO_2,
    CHANGE_SHELTER_3,
    ADD_MEMBER_4,
    REMOVE_MEMBER_5,
    BANK_TOKENS_OPERATION_6,
    CHANGE_ROLES_7,
    LEVEL_UP_8,
    SET_RELATION_KIND_9,
    BANK_ITEMS_OPERATION_10,
    SET_GUILD_PARAMS_11,
    CHANGE_PURCHASING_SHELTER_ITEMS_CAPACITY_12
  }

  enum GuildsParams {
    NONE_0,
    COUNTER_GUILD_IDS_1,
    BASE_FEE_2,
    COUNTER_GUILD_REQUESTS_3,
    REENTRANT_STATUS_4,
    SHELTER_CONTROLLER_5,
    SHELTER_AUCTION_6
  }

  enum GuildRequestStatus {
    NONE_0,
    ACCEPTED_1,
    REJECTED_2,
    CANCELED_3
  }

  /// @custom:storage-location erc7201:guild.controller.main
  struct MainState {
    /// @notice Mapping to store various guilds params (with global values for all guilds)
    mapping(GuildsParams param => uint value) guildsParam;

    /// @notice guildId => address of instance of GuildBank contract
    mapping(uint guildId => address) guildBanks;

    /// @notice guild id => guild data (owner, name, logo, etc)
    mapping(uint guildId => GuildData) guildData;

    /// @notice name => guild id
    mapping(string guildName => uint guildId) nameToGuild;

    /// @notice EOA => guild id, EOA can be a member of a single guild only
    mapping(address member => uint guildId) memberToGuild;

    /// @notice List of participants of guilds
    /// @dev Allowed number of members is 20 + 5 * guildLevel
    mapping(uint guildId => EnumerableSet.AddressSet listEoa) members;

    /// @notice Rights of the member in the guild, mask of GuildRightBits
    mapping(address member => uint maskRights) rights;

    /// @notice _getGuildsPairKey(guild1, guild2) => status (false - war, true - peace)
    mapping(bytes32 guildsPairKey => bool) relationsPeaceful;

    // ---------------------------- Request to join to the guild
    /// @notice Full list of requests registered for the guild
    mapping(uint guildId => mapping(GuildRequestStatus status => EnumerableSet.UintSet guildRequestIds)) guildRequests;

    /// @notice List of active requests created by the given user.
    /// "Active" => deposit should be returned to the user.
    /// All not-active requests are removed from here automatically.
    mapping(address user => EnumerableSet.UintSet guildRequestIds) userActiveGuildRequests;

    /// @notice Data of all guild requests ever created
    mapping(uint guildRequestId => GuildRequestData) guildRequestData;

    /// @notice Deposit amount required to create a guild request
    mapping(uint guildId => GuildRequestDeposit) guildRequestDepositAmounts;

    /// @notice Counter of spent pvp points + number of guild pvp-points allowed to be used by the guild member
    mapping(uint guildId => mapping(address member => UserPvpPoints)) userPvpPoints;

    /// @notice guild id => guildDescription
    mapping(uint guildId => string) guildDescription;
  }

  struct GuildData {
    /// @notice Not empty unique guild name
    string guildName;

    /// @notice URL of guild logo (empty is allowed)
    string urlLogo;

    /// @notice Creator (owner) of the guild
    address owner;

    /// @notice Guild level [1...10]
    uint8 guildLevel;

    /// @notice Percent of guild reinforcement fee Value in range [_FEE_MIN ... _TO_HELPER_RATIO_MAX], i.e. [10..50]
    uint8 toHelperRatio;

    /// @notice Global guild points counter, it's incremented on each victory in php-fight.
    /// @dev Assume here, that uint64 is enough to store any sums of scores
    uint64 pvpCounter;
  }

  struct GuildRequestData {
    GuildRequestStatus status;
    /// @notice Creator of the guild request that asks to include him to the guild
    address user;
    /// @notice Message to the guild owner from the user
    string userMessage;
    uint guildId;
  }

  struct GuildRequestDeposit {
    bool initialized;
    uint192 amount;
  }

  struct UserPvpPoints {
    /// @notice How many guild pvp-points the user is allowed to use
    uint64 capacityPvpPoints;

    /// @notice How many guild pvp-points the user has used
    uint64 spentPvpPoints;
  }

  /// ----------------------------------------------------------------------------------------------

  function memberOf(address user) external view returns (uint guildId);
  function guildToShelter(uint guildId) external view returns (uint shelterId);

  function getGuildData(uint guildId) external view returns (
    string memory guildName,
    string memory urlLogo,
    address owner,
    uint8 guildLevel,
    uint64 pvpCounter,
    uint toHelperRatio
  );

  function getRights(address user) external view returns (uint);
  function getGuildBank(uint guildId) external view returns (address);
  function shelterController() external view returns (address);

  function usePvpPoints(uint guildId, address user, uint64 priceInPvpPoints) external;
  function payFromGuildBank(uint guildId, uint shelterPrice) external;
  function payFromBalance(uint amount, address user) external;

  /// @notice Ensure that the {user} has given {right}, revert otherwise
  function checkPermissions(address user, uint right) external view returns (uint guildId, uint rights);
  function shelterAuctionController() external view returns (address);
  function payForAuctionBid(uint guildId, uint amount, uint bid) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;
import "../openzeppelin/EnumerableSet.sol";
import "../openzeppelin/EnumerableMap.sol";

interface IHeroController {

  /// @custom:storage-location erc7201:hero.controller.main
  struct MainState {

    /// @dev A central place for all hero tokens
    /// @dev Deprecated. Controller is used instead.
    address heroTokensVault;

    /// @dev heroAdr => packed tokenAdr160+ amount96
    mapping(address => bytes32) payToken;

    /// @dev heroAdr => heroCls8
    mapping(address => uint8) heroClass;

    // ---

    /// @dev hero+id => individual hero name
    mapping(bytes32 => string) heroName;

    /// @dev name => hero+id, needs for checking uniq names
    mapping(string => bytes32) nameToHero;

    // ---

    /// @dev hero+id => biome
    mapping(bytes32 => uint8) heroBiome;

    /// @notice Exist reinforcement of any kind for the given hero
    /// @dev hero+id => packed reinforcement helper+id
    mapping(bytes32 => bytes32) reinforcementHero;

    /// @dev hero+id => reinforcement packed attributes
    mapping(bytes32 => bytes32[]) reinforcementHeroAttributes;

    /// @notice packedHero (hero + id) => count of calls of beforeTokenTransfer
    mapping(bytes32 => uint) countHeroTransfers;


    // ------------------------------------ NG plus

    /// @notice (tier, hero address) => TierInfo, where tier = [2, 3]
    /// @dev For tier=1 no data is required. Amount for tier 1 is stored in {payToken}, no items are minted
    /// Token from {payToken} is equal for all tiers
    mapping(bytes32 packedTierHero => TierInfo) tiers;

    mapping(bytes32 packedHero => HeroInfo) heroInfo;

    /// @notice Max NG_LVL reached by the heroes of a given account
    mapping(address user => uint8 maxNgLevel) maxUserNgLevel;

    /// @notice When the hero has killed boss on the given biome first time
    /// packedBiomeNgLevel = packed (biome, NG_LEVEL)
    mapping(bytes32 packedHero => mapping (bytes32 packedBiomeNgLevel => uint timestamp)) killedBosses;

    /// @notice Max NG_LEVEL reached by any user
    uint maxOpenedNgLevel;
  }

  /// @notice Tier = hero creation cost option
  /// There are 3 tiers:
  /// 1: most chip option, just pay fixed amount {payTokens} - new hero is created
  /// 2: pay bigger amount - random skill is equipped on the newly created hero
  /// 3: pay even more amount - random sill + some random items are equipped on the newly created hero
  struct TierInfo {
    /// @notice Cost of the hero creation using the given tier in terms of the token stored in {payToken}
    /// This amount is used for tiers 2, 3. For tier 1 the amount is taken from {payToken}
    uint amount;

    /// @notice All slots for which items-to-mint are registered in {itemsToMint}
    EnumerableSet.UintSet slots;

    /// @notice slot => items that can be minted and equipped on the hero to the given {slot} after hero creation
    mapping(uint8 slot => address[] items) itemsToMint;
  }

  /// @notice Current NG+-related values
  struct HeroInfo {
    /// @notice Hero tier = [0..3]. 0 - the hero is post-paid, it can be changed by upgrading the hero to pre-paid
    uint8 tier;

    /// @notice NG_LVL of the hero
    uint8 ngLevel;

    /// @notice True if hero has passed last biome on current NG+ and so NG_LEVEL can be incremented (reborn is allowed)
    bool rebornAllowed;

    /// @notice Amount paid for the hero on creation OR on upgrade to NG+
    /// Amount paid for creation of the hero in terms of game token (!NG+) is NOT stored here.
    /// @dev uint72 is used here to pack the whole struct to single slot
    uint72 paidAmount;

    /// @notice Pay token used to pay {paidAmount}
    address paidToken;
  }

  /// @notice Input data to create new hero
  struct HeroCreationData {
    /// @notice Desired NG_LVL of the hero
    uint8 ngLevel;

    /// @notice Desired tire of the newly created hero. Allowed values: [1..3]
    uint8 tier;

    /// @notice Enter to the dungeon after creation
    bool enter;

    /// @notice Desired hero name
    string heroName;

    /// @notice Optional: user account for which the hero is created
    address targetUserAccount;

    /// @notice Optional: ref-code to be passed to the hero-creation-related event
    string refCode;
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  function heroClass(address hero) external view returns (uint8);

  function heroBiome(address hero, uint heroId) external view returns (uint8);

  function payTokenInfo(address hero) external view returns (address token, uint amount);

  function heroReinforcementHelp(address hero, uint heroId) external view returns (address helperHeroToken, uint helperHeroId);

  function score(address hero, uint heroId) external view returns (uint);

  function isAllowedToTransfer(address hero, uint heroId) external view returns (bool);

  function beforeTokenTransfer(address hero, uint heroId) external returns (bool);

  // ---

  function create(address hero, string memory heroName_, bool enter) external returns (uint);

  function kill(address hero, uint heroId) external returns (bytes32[] memory dropItems);

  /// @notice Take off all items from the hero, reduce life to 1. The hero is NOT burnt.
  /// Optionally reduce mana to zero and/or decrease life chance.
  function softKill(address hero, uint heroId, bool decLifeChances, bool resetMana) external returns (bytes32[] memory dropItems);

  function releaseReinforcement(address hero, uint heroId) external returns (address helperToken, uint helperId);

  function resetLifeAndMana(address hero, uint heroId) external;

  function countHeroTransfers(address hero, uint heroId) external view returns (uint);

  function askGuildReinforcement(address hero, uint heroId, address helper, uint helperId) external;

  function getHeroInfo(address hero, uint heroId) external view returns (IHeroController.HeroInfo memory data);

  function registerKilledBoss(address hero, uint heroId, uint32 objectId) external;

  function maxOpenedNgLevel() external view returns (uint);
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IItem {

  function isItem() external pure returns (bool);

  function mintFor(address recipient) external returns (uint tokenId);

  function burn(uint tokenId) external;

  function controlledTransfer(address from, address to, uint tokenId) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "./IStatController.sol";
import "./IGOC.sol";
import "../openzeppelin/EnumerableSet.sol";

interface IItemController {

  enum GlobalParam {
    UNKNOWN_0,

    /// @notice Address of ItemControllerHelper
    ITEM_CONTROLLER_HELPER_ADDRESS_1
  }

  /// @custom:storage-location erc7201:item.controller.main
  struct MainState {

    ////////////////// GENERATE //////////////////

    EnumerableSet.AddressSet items;

    /// @dev itemAdr => itemMetaType8 + itemLvl8 + itemType8 + baseDurability16 + defaultRarity8 + minAttr8 + maxAttr8 + manaCost32 + req(packed core 128)
    mapping(address => bytes32) itemMeta;

    /// @dev itemAdr => packed tokenAdr160+ amount96
    mapping(address => bytes32) augmentInfo;

    // --- common attr ---

    /// @dev itemAdr => id8 + min(int32) + max(int32) + chance32
    mapping(address => bytes32[]) generateInfoAttributes;

    // --- consumable ---

    /// @dev itemAdr => ids+values (toBytes32ArrayWithIds)
    mapping(address => bytes32[]) _itemConsumableAttributes;

    /// @dev itemAdr => IStatController.ChangeableStats packed int32[]
    mapping(address => bytes32) itemConsumableStats;

    // --- buff ---

    /// @dev itemAdr => id8 + min(int32) + max(int32) + chance32
    mapping(address => bytes32[]) generateInfoCasterAttributes;

    /// @dev itemAdr => id8 + minDmg(int32) + maxDmg(int32) + chance32
    mapping(address => bytes32[]) generateInfoTargetAttributes;

    // --- attack ---

    /// @dev itemAdr => packed AttackInfo: attackType8 + min32 + max32 + factors(packed core 128)
    mapping(address => bytes32) generateInfoAttack;

    ////////////////// ITEMS INFO //////////////////

    /// @dev itemAdr+id => itemRarity8 + augmentationLevel8 + itemDurability16
    mapping(bytes32 => bytes32) itemInfo;

    /// @dev itemAdr+id => heroAdr+id
    mapping(bytes32 => bytes32) equippedOn;

    // --- common attr ---

    /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds)
    mapping(bytes32 => bytes32[]) _itemAttributes;

    // --- consumable ---

    // consumable stats unchangeable, get them by address

    // --- buff ---

    /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds)
    mapping(bytes32 => bytes32[]) _itemCasterAttributes;

    /// @dev itemAdr+Id => ids+values (toBytes32ArrayWithIds)
    mapping(bytes32 => bytes32[]) _itemTargetAttributes;

    // --- attack ---

    /// @dev itemAdr+Id => packed AttackInfo: attackType8 + min32 + max32 + factors(packed core 128)
    mapping(bytes32 => bytes32) _itemAttackInfo;

    ////////////////// Additional generate info //////////////////

    /// @notice (itemAdr) => Bitmask of ConsumableActionBits
    mapping(address => uint) _consumableActionMask;


    /// --------------------------------- SIP-003: Item fragility
    /// @notice itemAdr + id => item fragility counter that displays the chance of an unsuccessful repair
    /// @dev [0...100_000], decimals 3
    mapping(bytes32 packedItem => uint fragility) itemFragility;

    /// @notice Universal mapping to store various addresses and numbers (params of the contract)
    mapping (GlobalParam param => uint value) globalParam;

    /// @notice Item address => packedMetadata
    /// {packedMetaData} is encoded using abi.encode/abi.decode
    /// Read first byte, detect meta data type by the byte value, apply proper decoder from PackingLib
    mapping(address item => bytes packedMetaData) packedItemMetaData;
  }

  struct RegisterItemParams {
    ItemMeta itemMeta;
    address augmentToken;
    uint augmentAmount;
    ItemGenerateInfo commonAttributes;

    IGOC.AttributeGenerateInfo consumableAttributes;
    IStatController.ChangeableStats consumableStats;

    ItemGenerateInfo casterAttributes;
    ItemGenerateInfo targetAttributes;

    AttackInfo genAttackInfo;
    /// @notice Bit mask of ConsumableActionBits
    uint consumableActionMask;
  }

  /// @notice Possible actions that can be triggered by using the consumable item
  enum ConsumableActionBits {
    CLEAR_TEMPORARY_ATTRIBUTES_0
    // other items are used instead this mask
  }

  struct ItemGenerateInfo {
    /// @notice Attribute ids
    uint8[] ids;
    /// @notice Min value of the attribute, != 0
    int32[] mins;
    /// @notice Max value of the attribute, != 0
    int32[] maxs;
    /// @notice Chance of the selection [0..MAX_CHANCES]
    uint32[] chances;
  }

  struct ItemMeta {
    uint8 itemMetaType;
    // Level in range 1-99. Reducing durability in low level dungeons. lvl/5+1 = biome
    uint8 itemLevel;
    IItemController.ItemType itemType;
    uint16 baseDurability;
    uint8 defaultRarity;
    uint32 manaCost;

    // it doesn't include positions with 100% chance
    uint8 minRandomAttributes;
    uint8 maxRandomAttributes;

    IStatController.CoreAttributes requirements;
  }

  // Deprecated. Todo - remove
  enum FeeType {
    UNKNOWN,
    REPAIR,
    AUGMENT,
    STORY,

    END_SLOT
  }

  enum ItemRarity {
    UNKNOWN, // 0
    NORMAL, // 1
    MAGIC, // 2
    RARE, // 3
    SET, // 4
    UNIQUE, // 5

    END_SLOT
  }

  enum ItemType {
    NO_SLOT, // 0
    HEAD, // 1
    BODY, // 2
    GLOVES, // 3
    BELT, // 4
    AMULET, // 5
    RING, // 6
    OFF_HAND, // 7
    BOOTS, // 8
    ONE_HAND, // 9
    TWO_HAND, // 10
    SKILL, // 11
    OTHER, // 12

    END_SLOT
  }

  enum ItemMetaType {
    UNKNOWN, // 0
    COMMON, // 1
    ATTACK, // 2
    BUFF, // 3
    CONSUMABLE, // 4

    END_SLOT
  }

  enum AttackType {
    UNKNOWN, // 0
    FIRE, // 1
    COLD, // 2
    LIGHTNING, // 3
    CHAOS, // 4

    END_SLOT
  }

  struct AttackInfo {
    AttackType aType;
    int32 min;
    int32 max;
    // if not zero - activate attribute factor for the attribute
    IStatController.CoreAttributes attributeFactors;
  }

  struct ItemInfo {
    ItemRarity rarity;
    uint8 augmentationLevel;
    uint16 durability;
  }

  /// @dev The struct is used in events, so it's moved here from the lib
  struct MintInfo {
    IItemController.ItemMeta meta;
    uint8[] attributesIds;
    int32[] attributesValues;
    IItemController.ItemRarity itemRarity;

    IItemController.AttackInfo attackInfo;

    uint8[] casterIds;
    int32[] casterValues;
    uint8[] targetIds;
    int32[] targetValues;
  }

  /// @dev The struct is used in events, so it's moved here from the lib
  struct AugmentInfo {
    uint8[] attributesIds;
    int32[] attributesValues;
    IItemController.AttackInfo attackInfo;
    uint8[] casterIds;
    int32[] casterValues;
    uint8[] targetIds;
    int32[] targetValues;
  }

  ///region ------------------------ Item type "Other"
  /// @notice Possible kinds of "Other" items
  /// Each "Other" item has each own structure for metadata, see OtherItemXXX
  enum OtherSubtypeKind {
    UNKNOWN_0,
    /// @notice Item to reduce fragility, see SCB-1014. Metadata is {OtherItemReduceFragility}
    REDUCE_FRAGILITY_1,

    /// @notice This item allows asking guild reinforcement to the guild member
    USE_GUILD_REINFORCEMENT_2,

    /// @notice Exit from dungeon (shelter of level 3 is required)
    EXIT_FROM_DUNGEON_3,

    /// @notice Rest in the shelter: restore of hp & mp, clear temporally attributes, clear used consumables (shelter of level 3 is required)
    REST_IN_SHELTER_4,

    /// @notice Stub item (i.e. OTHER_4) that has no logic in contracts, but it has correct (not empty) packedMetaData
    EMPTY_NO_LOGIC_5,

    END_SLOT
  }
  struct OtherItemReduceFragility {
    /// @notice "Other" item kind. It MUST BE first field in the struct.
    uint8 kind;

    /// @notice Value on which the fragility will be reduced.
    /// @dev [0...100%], decimals 3, so the value is in the range [0...10_000]
    uint248 value;
  }
  ///endregion ------------------------ Item type "Other"

  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  function itemMeta(address item) external view returns (ItemMeta memory meta);

  function augmentInfo(address item) external view returns (address token, uint amount);

  function genAttributeInfo(address item) external view returns (ItemGenerateInfo memory info);

  function genCasterAttributeInfo(address item) external view returns (ItemGenerateInfo memory info);

  function genTargetAttributeInfo(address item) external view returns (ItemGenerateInfo memory info);

  function genAttackInfo(address item) external view returns (AttackInfo memory info);

  function itemInfo(address item, uint itemId) external view returns (ItemInfo memory info);

  function equippedOn(address item, uint itemId) external view returns (address hero, uint heroId);

  function itemAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids);

  function consumableAttributes(address item) external view returns (int32[] memory values, uint8[] memory ids);

  function consumableStats(address item) external view returns (IStatController.ChangeableStats memory stats);

  function casterAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids);

  function targetAttributes(address item, uint itemId) external view returns (int32[] memory values, uint8[] memory ids);

  function itemAttackInfo(address item, uint itemId) external view returns (AttackInfo memory info);

  function score(address item, uint tokenId) external view returns (uint);

  function isAllowedToTransfer(address item, uint tokenId) external view returns (bool);

  // ---

  function mint(address item, address recipient) external returns (uint itemId);

  function reduceDurability(address hero, uint heroId, uint8 biome, bool reduceDurabilityAllSkills) external;

  function destroy(address item, uint tokenId) external;

  function takeOffDirectly(
    address item,
    uint itemId,
    address hero,
    uint heroId,
    uint8 itemSlot,
    address destination,
    bool broken
  ) external;

  /// @notice SIP-003: item fragility counter that displays the chance of an unsuccessful repair.
  /// @dev [0...100%], decimals 3, so the value is in the range [0...10_000]
  function itemFragility(address item, uint itemId) external view returns (uint);

  /// @notice SIP-003: The quest mechanic that previously burned the item will increase its fragility by 1%
  function incBrokenItemFragility(address item, uint itemId) external;

  function equip(
    address hero,
    uint heroId,
    address[] calldata items,
    uint[] calldata itemIds,
    uint8[] calldata itemSlots
  ) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IOracle {

  function getRandomNumber(uint max, uint seed) external returns (uint);

  function getRandomNumberInRange(uint min, uint max, uint seed) external returns (uint);

}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "./IStatController.sol";
import "../openzeppelin/EnumerableMap.sol";

/// @notice Terms
/// Reinforcement v1: helper is selected randomly in askHero, fixed part of rewards (tokens and NFT) is sent to the helper.
/// Guild reinforcement: helper is selected from guild heroes. Rewards are sent to guild bank.
/// Reinforcement v2: helper is selected manually in askHeroV2, helper receives fixed amount.
interface IReinforcementController {

  enum ConfigParams {
    /// @notice Packed MinMaxBoardV2
    V2_MIN_MAX_BOARD_0
  }

  /// @custom:storage-location erc7201:reinforcement.controller.main
  struct MainState {

    // ------------------------ Reinforcement v1

    /// @dev minLvl8 + minLifeChances8
    bytes32 config;
    /// @dev hero token + hero id => heroInfo(biome8 + score128 + fee8 + stakeTs64)
    mapping(bytes32 => bytes32) _stakedHeroes;
    /// @dev biome => helperAdr+id
    mapping(uint => EnumerableSet.Bytes32Set) _internalIdsByBiomes;
    /// @dev biome => score  // The field is deprecated and not updated any more
    mapping(uint => uint) maxScore;
    /// @dev heroAdr+id => itemAdr+id
    mapping(bytes32 => bytes32[]) _heroNftRewards;
    /// @dev heroAdr+id => tokenAdr and amount map
    mapping(bytes32 => EnumerableMap.AddressToUintMap) _heroTokenRewards;


    // ------------------------ Guild reinforcement

    /// @notice All staked guild heroes for the given guild
    /// @dev helper (hero token + hero id) => guild
    mapping(bytes32 packedHero => uint guildId) stakedGuildHeroes;

    /// @notice All guild heroes that are currently in use by guild reinforcement
    /// It's allowed to withdraw a hero before reinforcement releasing,
    /// so it's possible to have !0 in {guildBusyHelpers} and 0 in {stakedGuildHeroes} simultaneously.
    /// @dev helper (hero token + hero id) => guildId (guild at the moment of askGuildReinforcement)
    mapping(bytes32 packedHero => uint guildId) busyGuildHelpers;

    /// @notice All (free and busy) staked guild heroes per guild.
    /// guild => (packed helper => guild where the helper is busy currently)
    /// @dev There is a chance that guilds are different here
    /// i.e. hero can be:
    /// 1) added to G1 2) staked in G1 3) asked for help 4) withdrawn 5) G1=>G2 6) staked in G2
    /// In such case guildHelpers[G2][hero] = G1, guildHelpers[G1][hero] = 0
    /// After releasing guildHelpers[G2][hero] = 0
    mapping(uint guildId => EnumerableMap.Bytes32ToUintMap) guildHelpers;

    /// @notice Moment of withdrawing the hero from staking. Next staking is possible in 1 day since withdrawing
    mapping(bytes32 packedHero => uint lastWithdrawTimestamp) lastGuildHeroWithdrawTs;


    // ------------------------ Reinforcement v2
    /// @notice Map to store various config params
    mapping(ConfigParams paramId => uint) configParams;

    mapping(bytes32 packedHero => HeroInfoV2) stakedHeroesV2;

    /// @notice biome => set of packedHero. All staked heroes (they can be busy of free currently)
    mapping(uint biome => EnumerableSet.Bytes32Set) heroesByBiomeV2;

    mapping(uint biome => LastWindowsV2) stat24hV2;
  }


  /// @notice Deprecated. Reinforcement v1
  struct HeroInfo {
    uint8 biome;
    uint score; // stored in 128 but easy to use 256
    /// @notice To helper ratio
    uint8 fee;
    uint64 stakeTs;
  }

  struct HeroInfoV2 {
    uint8 biome;
    uint64 stakeTs;
    /// @notice Amount of game token that is paid to the helper at the moment of the call {askHeroV2}
    uint128 rewardAmount;
  }

  /// @notice Statistic of askHeroV2 calls per last 24 hours at the moment of the last call
  struct LastWindowsV2 {
    /// @notice 24 hours are divided on 8 intervals, each interval is 3 hour
    /// Current basket has index {basketIndex}
    /// {baskets[current basket]} contains "old" value.
    /// New value for the current basket is collected in {basketValue}.
    /// The value for the current basket is calculated as weighted average of old and new values.
    /// New value replaces the old value at the moment of changing current basket index.
    uint24[8] baskets;
    /// @notice New value (hits counter) for current basket
    uint24 basketValue;
    /// @notice Abs. index of the current basket (abs. hour / 3)
    uint48 basketIndex;
  }

  /// @dev 1 slot
  struct ConfigReinforcementV2 {
    /// @notice if Number-of-askHeroV2-calls is below given value then burn fee has min value
    uint32 minNumberHits;
    /// @notice if Number-of-askHeroV2-calls is above given value then burn fee has max value
    uint32 maxNumberHits;
    /// @notice Lowest fee = amountForDungeon / given value, i.e. 100 => amountForDungeon/100 as lower fee
    uint32 lowDivider;
    /// @notice Highest fee = amountForDungeon / given value, i.e. 2 => amountForDungeon/2 as highest fee
    uint32 highDivider;
    /// @notice Limit for min level of the staked hero
    /// In practice we need following limitation: (stats.level < 5 || (stats.level - 5) / 5 < biome)
    /// so, levelLimit should be equal 5
    /// In tests we need to be able to disable such limitation, so levelLimit = 0 allow to disable that constraint
    uint8 levelLimit;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  function toHelperRatio(address heroToken, uint heroId) external view returns (uint);

  function isStaked(address heroToken, uint heroId) external view returns (bool);

  function registerTokenReward(address heroToken, uint heroId, address token, uint amount) external;

  function registerNftReward(address heroToken, uint heroId, address token, uint tokenId) external;

  function askHeroV2(address hero, uint heroId, address helper, uint helperId) external returns (int32[] memory attributes);

  function askGuildHero(address hero, uint heroId, address helper, uint helperId) external returns (int32[] memory attributes);

  /// @notice Return the guild in which the hero is currently asked for guild reinforcement
  function busyGuildHelperOf(address heroToken, uint heroId) external view returns (uint guildId);

  function releaseGuildHero(address helperHeroToken, uint helperHeroTokenId) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IRewardsPool {

  /// @custom:storage-location erc7201:rewards.pool.main
  struct MainState {
    mapping(address token => uint baseAmountValue) baseAmounts;
  }

  function balanceOfToken(address token) external view returns (uint);

  function rewardAmount(address token, uint maxBiome, uint maxNgLevel, uint biome, uint heroNgLevel) external view returns (uint);

  function sendReward(address token, uint rewardAmount_, address receiver) external;

  function lostProfitPercent(uint maxBiome, uint maxNgLevel, uint heroNgLevel) external view returns (uint percent);
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;
import "../openzeppelin/EnumerableSet.sol";
import "../openzeppelin/EnumerableMap.sol";

interface IShelterAuction {
  enum ShelterAuctionParams {
    NONE_0,
    POSITION_COUNTER_1,
    BID_COUNTER_2,
    FEE_3
  }

  //region ------------------------ Data types
  /// @custom:storage-location erc7201:shelter.auction.main
  struct MainState {

    /// @notice Mapping to store auction params (i.e. counters)
    mapping(ShelterAuctionParams param => uint value) params;

    /// @notice Hold all positions. Any record should not be removed
    mapping(uint positionId => Position) positions;

    /// @dev BidId => Bid. Hold all bids. Any record should not be removed
    mapping(uint bidId => AuctionBid) auctionBids;

    /// @notice List of currently opened positions
    EnumerableSet.UintSet openPositions;

    /// @notice Seller to position map
    /// At any moment each guild can have only one opened position to sell
    mapping(uint sellerGuildId => uint openedPositionId) sellerPosition;

    /// @notice Position that the buyer is going to purchase.
    /// At any moment each guild can have only one opened position to purchase
    mapping(uint buyerGuildId => BuyerPositionData) buyerPosition;

    /// @notice All open and close bids for the given position
    mapping(uint positionId => uint[] bidIds) positionToBidIds;

    /// @notice Timestamp of the last bid for the auction
    mapping(uint positionId => uint timestamp) lastAuctionBidTs;
}

  struct Position {
    bool open;
    /// @notice User that opens the position. The user belongs to the guild with id = {sellerGuildId}
    address seller;

    /// @notice Assume that shelter can be stored as uint64
    uint64 shelterId;

    uint128 positionId;

    /// @notice Min allowed (initial) auction price. Only first bid is able to use it.
    uint128 minAuctionPrice;

    uint128 sellerGuildId;
  }

  struct AuctionBid {
    bool open;
    /// @notice User that opens the bid. The user belongs to the guild with id = {buyerGuildId}
    address buyer;

    uint128 bidId;
    uint128 positionId;
    /// @notice Bid amount in terms of game token. This amount is transferred from guild Bank to ShelterAuction balance
    uint128 amount;
    uint128 buyerGuildId;
  }

  struct BuyerPositionData {
    /// @notice ID of the position that the buyer is going to purchase
    uint128 positionId;

    /// @notice 0-based index of the opened bid in {positionToBidIds}
    uint128 bidIndex;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

  function positionBySeller(uint sellerGuildId_) external view returns (uint positionId);
  function positionByBuyer(uint buyerGuildId) external view returns (uint positionId, uint bidIndex);
  function posByShelter(uint shelterId_) external view returns (uint positionId);
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../openzeppelin/EnumerableSet.sol";

interface IShelterController {
  /// @custom:storage-location erc7201:shelter.controller.main
  struct MainState {
    /// @notice List of items allowed to be purchased in the shelter
    mapping(uint shelterId => EnumerableSet.AddressSet) shelterItems;

    /// @notice Data of items available for purchasing in the given shelter
    mapping(uint shelterId => mapping(address item => ShelterItemData)) shelterItemData;

    // @notice Statistics how much items were purchased per day
    mapping(uint shelterId => mapping(uint32 epochDay => mapping(address item => uint))) countPurchasedItems;

    /// @notice List of registered shelters in {biome}
    mapping(uint biome => EnumerableSet.UintSet shelterUids) shelters;

    /// @notice Initial price of the shelters in game tokens
    mapping(uint shelterId => uint) shelterPrices;

    /// @notice Shelters belong to a specific guild (not the player)
    /// Shelters can be free (don't belong to any guild)
    mapping(uint shelterId => uint guildId) shelterToGuild;

    /// @notice Each guild can own 0 or 1 shelter
    mapping(uint guildId => uint shelterId) guildToShelter;
  }

  struct ShelterItemData {
    /// @notice Price of the item in pvp-points
    uint64 priceInPvpPoints;
    /// @notice Price of the item game token
    uint128 priceInGameToken;
    /// @notice Max number of items that can be purchases per day in the shelter. 0 - no limitations
    uint16 maxItemsPerDayLimit;
  }

  /// ----------------------------------------------------------------------------------------------

  function clearShelter(uint guildId) external;
  function guildToShelter(uint guildId) external view returns (uint shelterId);
  function changeShelterOwner(uint shelterId, uint newOwnerGuildId) external;
  function shelterToGuild(uint shelterId) external view returns (uint guildId);
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;
import "../openzeppelin/EnumerableSet.sol";
import "../openzeppelin/EnumerableMap.sol";

interface IStatController {

  /// @custom:storage-location erc7201:stat.controller.main
  struct MainState {
    mapping(bytes32 => bytes32[]) heroTotalAttributes;
    /// @dev heroAdr+heroId => int32 packed strength, dexterity, vitality, energy
    mapping(bytes32 => bytes32) _heroCore;
    mapping(bytes32 => bytes32[]) heroBonusAttributes;
    mapping(bytes32 => bytes32[]) heroTemporallyAttributes;
    /// @dev heroAdr+heroId => uint32 packed level, experience, life, mana, lifeChances
    mapping(bytes32 => bytes32) heroStats;
    /// @dev heroAdr+heroId+itemSlot => itemAdr + itemId
    mapping(bytes32 => bytes32) heroSlots;
    /// @dev heroAdr+heroId => busy slots uint8[] packed
    mapping(bytes32 => bytes32) heroBusySlots;
    mapping(bytes32 => EnumerableSet.AddressSet) usedConsumables;
    /// @dev heroCustomDataV2 is used instead
    mapping(bytes32 => mapping(bytes32 => uint)) _deprecated_heroCustomData;
    mapping(bytes32 => uint) globalCustomData;

    /// @notice packNftIdWithValue(hero, heroId, ngLevel) => hero custom data map
    /// @dev initially it was packedHero => hero custom data map
    mapping(bytes32 => EnumerableMap.Bytes32ToUintMap) heroCustomDataV2;
  }


  enum ATTRIBUTES {
    // core
    STRENGTH, // 0
    DEXTERITY, // 1
    VITALITY, // 2
    ENERGY, // 3
    // attributes
    DAMAGE_MIN, // 4
    DAMAGE_MAX, // 5
    ATTACK_RATING, // 6
    DEFENSE, // 7
    BLOCK_RATING, // 8
    LIFE, // 9
    MANA, // 10
    // resistance
    FIRE_RESISTANCE, // 11
    COLD_RESISTANCE, // 12
    LIGHTNING_RESISTANCE, // 13
    // dmg against
    DMG_AGAINST_HUMAN, // 14
    DMG_AGAINST_UNDEAD, // 15
    DMG_AGAINST_DAEMON, // 16
    DMG_AGAINST_BEAST, // 17

    // defence against
    DEF_AGAINST_HUMAN, // 18
    DEF_AGAINST_UNDEAD, // 19
    DEF_AGAINST_DAEMON, // 20
    DEF_AGAINST_BEAST, // 21

    // --- unique, not augmentable
    // hero will not die until have positive chances
    LIFE_CHANCES, // 22
    // increase chance to get an item
    MAGIC_FIND, // 23
    // decrease chance to get an item
    DESTROY_ITEMS, // 24
    // percent of chance x2 dmg
    CRITICAL_HIT, // 25
    // dmg factors
    MELEE_DMG_FACTOR, // 26
    FIRE_DMG_FACTOR, // 27
    COLD_DMG_FACTOR, // 28
    LIGHTNING_DMG_FACTOR, // 29
    // increase attack rating on given percent
    AR_FACTOR, // 30
    // percent of damage will be converted to HP
    LIFE_STOLEN_PER_HIT, // 31
    // amount of mana restored after each battle
    MANA_AFTER_KILL, // 32
    // reduce all damage on percent after all other reductions
    DAMAGE_REDUCTION, // 33

    // -- statuses
    // chance to stun an enemy, stunned enemy skip next hit
    STUN, // 34
    // chance burn an enemy, burned enemy will loss 50% of defence
    BURN, // 35
    // chance freeze an enemy, frozen enemy will loss 50% of MELEE damage
    FREEZE, // 36
    // chance to reduce enemy's attack rating on 50%
    CONFUSE, // 37
    // chance curse an enemy, cursed enemy will loss 50% of resistance
    CURSE, // 38
    // percent of dmg return to attacker
    REFLECT_DAMAGE_MELEE, // 39
    REFLECT_DAMAGE_MAGIC, // 40
    // chance to poison enemy, poisoned enemy will loss 10% of the current health
    POISON, // 41
    // reduce chance get any of uniq statuses
    RESIST_TO_STATUSES, // 42

    END_SLOT // 43
  }

  // possible
  // HEAL_FACTOR

  struct CoreAttributes {
    int32 strength;
    int32 dexterity;
    int32 vitality;
    int32 energy;
  }

  struct ChangeableStats {
    uint32 level;
    uint32 experience;
    uint32 life;
    uint32 mana;
    uint32 lifeChances;
  }

  enum ItemSlots {
    UNKNOWN, // 0
    HEAD, // 1
    BODY, // 2
    GLOVES, // 3
    BELT, // 4
    AMULET, // 5
    BOOTS, // 6
    RIGHT_RING, // 7
    LEFT_RING, // 8
    RIGHT_HAND, // 9
    LEFT_HAND, // 10
    TWO_HAND, // 11
    SKILL_1, // 12
    SKILL_2, // 13
    SKILL_3, // 14
    END_SLOT // 15
  }

  struct NftItem {
    address token;
    uint tokenId;
  }

  enum Race {
    UNKNOWN, // 0
    HUMAN, // 1
    UNDEAD, // 2
    DAEMON, // 3
    BEAST, // 4
    END_SLOT // 5
  }

  struct ChangeAttributesInfo {
    address heroToken;
    uint heroTokenId;
    int32[] changeAttributes;
    bool add;
    bool temporally;
  }

  struct BuffInfo {
    address heroToken;
    uint heroTokenId;
    uint32 heroLevel;
    address[] buffTokens;
    uint[] buffTokenIds;
  }

  /// @dev This struct is used inside event, so it's moved here from lib
  struct ActionInternalInfo {
    int32[] posAttributes;
    int32[] negAttributes;

    uint32 experience;
    int32 heal;
    int32 manaRegen;
    int32 lifeChancesRecovered;
    int32 damage;
    int32 manaConsumed;

    address[] mintedItems;
  }

  function initNewHero(address token, uint tokenId, uint heroClass) external;

  function heroAttributes(address token, uint tokenId) external view returns (int32[] memory);

  function heroAttribute(address token, uint tokenId, uint index) external view returns (int32);

  function heroAttributesLength(address token, uint tokenId) external view returns (uint);

  function heroBaseAttributes(address token, uint tokenId) external view returns (CoreAttributes memory);

  function heroCustomData(address token, uint tokenId, bytes32 index) external view returns (uint);

  function globalCustomData(bytes32 index) external view returns (uint);

  function heroStats(address token, uint tokenId) external view returns (ChangeableStats memory);

  function heroItemSlot(address token, uint64 tokenId, uint8 itemSlot) external view returns (bytes32 nftPacked);

  function heroItemSlots(address heroToken, uint heroTokenId) external view returns (uint8[] memory);

  function isHeroAlive(address heroToken, uint heroTokenId) external view returns (bool);

  function levelUp(address token, uint tokenId, uint heroClass, CoreAttributes memory change) external returns (uint newLvl);

  function changeHeroItemSlot(
    address heroToken,
    uint64 heroTokenId,
    uint itemType,
    uint8 itemSlot,
    address itemToken,
    uint itemTokenId,
    bool equip
  ) external;

  function changeCurrentStats(
    address token,
    uint tokenId,
    ChangeableStats memory change,
    bool increase
  ) external;

  function changeBonusAttributes(ChangeAttributesInfo memory info) external;

  function registerConsumableUsage(address heroToken, uint heroTokenId, address item) external;

  function clearUsedConsumables(address heroToken, uint heroTokenId) external;

  function clearTemporallyAttributes(address heroToken, uint heroTokenId) external;

  function buffHero(BuffInfo memory info) external view returns (int32[] memory attributes, int32 manaConsumed);

  function setHeroCustomData(address token, uint tokenId, bytes32 index, uint value) external;

  function setGlobalCustomData(bytes32 index, uint value) external;

  /// @notice Restore life and mana during reinforcement
  /// @dev Life and mana will be increased on ((current life/mana attr value) - (prev life/mana attr value))
  /// @param prevAttributes Hero attributes before reinforcement
  function restoreLifeAndMana(address heroToken, uint heroTokenId, int32[] memory prevAttributes) external;

  function reborn(address heroToken, uint heroTokenId, uint heroClass) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "../interfaces/IGOC.sol";
import "../interfaces/IStatController.sol";
import "../interfaces/IItemController.sol";
import "./IController.sol";
import "./IOracle.sol";
import "./IHeroController.sol";
import "../openzeppelin/EnumerableSet.sol";

interface IStoryController {

  enum AnswerResultId {
    UNKNOWN, // 0
    SUCCESS, // 1
    ATTRIBUTE_FAIL, // 2
    RANDOM_FAIL, // 3
    DELAY_FAIL, // 4
    HERO_CUSTOM_DATA_FAIL, // 5
    GLOBAL_CUSTOM_DATA_FAIL, // 6

    END_SLOT
  }

  enum CustomDataResult {
    UNKNOWN, // 0
    HERO_SUCCESS, // 1
    HERO_FAIL, // 2
    GLOBAL_SUCCESS, // 3
    GLOBAL_FAIL, // 4

    END_SLOT
  }

  /// @custom:storage-location erc7201:story.controller.main
  struct MainState {

    // --- STORY REG INFO ---

    /// @dev Uniq story identification.
    mapping(uint32 => uint16) storyIds;
    /// @dev Revers mapping for stories for using in the next object rewrite logic.
    mapping(uint16 => uint32) idToStory;
    /// @dev Store used ids for stories.
    mapping(uint16 => bool) _usedStoryIds;
    /// @dev Prevent register the story twice
    mapping(uint32 => bool) registeredStories;

    // --- ANSWER MAPPING ---

    /// @dev storyId => all story pages. We need to have this mapping for properly remove meta info
    mapping(uint16 => EnumerableSet.UintSet) allStoryPages;

    /// @dev storyId => all possible answers. We need to have this mapping for properly remove meta info
    mapping(uint16 => EnumerableSet.Bytes32Set) allStoryAnswers;

    /// @dev storyId + pageId + heroClass (zero is default answers) => storyId + pageId + heroClass (zero is default answers) + answerId
    mapping(bytes32 => bytes32[]) answers;
    /// @dev answerUnPackedId + answerResultId => nextPageIds (will be chosen randomly from this array)
    ///      where answerResultId is:
    ///      0 - unknown,
    ///      1 - success,
    ///      2 - attr fail
    ///      3 - random fail
    ///      4 - delay fail
    ///      5 - hero custom data fail
    ///      6 - global custom data fail
    ///      see COUNT_ANSWER_RESULT_IDS
    mapping(bytes32 => uint16[]) nextPageIds;
    /// @dev story + pageId + heroClass (zero is default answers) => random nextObjs (adr + id, like packed nft id)
    mapping(bytes32 => uint32[]) nextObjectsRewrite;

    /// @dev answerPackedId => packed array of uint32[]
    ///      0 - random requirement(uint32, 1 - 99% success of this action, zero means no check)
    ///      1 - delay requirement(uint32, if time since the last call more than this value the check is fail, zero means no check)
    ///      2 - isFinalAnswer(uint8)
    mapping(bytes32 => bytes32) answerAttributes;

    // --- ANSWER REQUIREMENTS ---

    /// @dev answerPackedId => array of AttributeRequirementsPacked
    mapping(bytes32 => bytes32[]) attributeRequirements;
    /// @dev answerPackedId=> array of ItemRequirementsPacked
    mapping(bytes32 => bytes32[]) itemRequirements;
    /// @dev answerPackedId => array of TokenRequirementsPacked
    mapping(bytes32 => bytes32[]) tokenRequirements;
    /// @dev answerPackedId => custom data for hero
    mapping(bytes32 => CustomDataRequirementPacked[]) heroCustomDataRequirement;
    /// @dev answerPackedId => global custom data
    mapping(bytes32 => CustomDataRequirementPacked[]) globalCustomDataRequirement;

    // --- ANSWER RESULTS ---

    /// @dev answerPackedId => change attributes
    mapping(bytes32 => bytes32[]) successInfoAttributes;
    /// @dev answerPackedId => change stats
    mapping(bytes32 => bytes32) successInfoStats;
    /// @dev answerPackedId => mint items
    mapping(bytes32 => bytes32[]) successInfoMintItems;

    /// @dev answerPackedId => change attributes
    mapping(bytes32 => bytes32[]) failInfoAttributes;
    /// @dev answerPackedId => change stats
    mapping(bytes32 => bytes32) failInfoStats;
    /// @dev answerPackedId => mint items
    mapping(bytes32 => bytes32[]) failInfoMintItems;

    /// @dev answerUnPackedId + CustomDataResult => custom data array change
    ///      where CustomDataResult is
    ///      1 - hero success
    ///      2 - hero fail
    ///      3 - global success
    ///      4 - global fail
    ///      see COUNT_CUSTOM_DATA_RESULT_IDS
    mapping(bytes32 => bytes32[]) customDataResult;

    /// @notice answerPackedId => slot+chance+stopIfBurnt
    /// @dev Since SIP-003 the items are not burn but broke
    mapping(bytes32 => bytes32[]) burnItem;

    // --- GENERAL STORY REQUIREMENTS ---

    /// @dev story => Custom hero data requirements for a story. If exist and hero is not eligible should be not chose in a dungeon.
    mapping(uint => CustomDataRequirementRangePacked[]) storyRequiredHeroData;
    /// @dev story => Minimal level for the history. 0 means no requirements.
    mapping(uint => uint) storyRequiredLevel;

    // --- HERO STATES ---

    /// @dev hero + heroId + storyId => pageId + heroLastActionTS
    mapping(bytes32 => bytes32) heroState;

    // --- OTHER ---

    /// @dev storyId => build hash for the last update
    mapping(uint16 => uint) storyBuildHash;

    /// @notice Number of already minted items by the user within the given iteration of the story.
    /// Only minting of the given number of items is allowed per iteration (see MAX_MINTED_ITEMS_PER_ITERATION).
    /// @dev hero, heroId, story => mintedInIteration
    /// This map is not cleared: storyId:objectId is 1:1, each object has own sequence of iterations without duplicates
    mapping(bytes32 => mapping(uint iteration => uint countMintedItems)) mintedInIteration;
  }

  /// @dev We need to have flat structure coz Solidity can not handle arrays of structs properly
  struct StoryMetaInfo {
    uint16 storyId;

    // --- story reqs

    bytes32[] requiredCustomDataIndex;
    uint64[] requiredCustomDataMinValue;
    uint64[] requiredCustomDataMaxValue;
    bool[] requiredCustomDataIsHero;
    uint minLevel;

    // --- answer reqs

    AnswersMeta answersMeta;
    AnswerNextPageMeta answerNextPage;
    AnswerAttributeRequirementsMeta answerAttributeRequirements;
    AnswerItemRequirementsMeta answerItemRequirements;
    AnswerTokenRequirementsMeta answerTokenRequirements;
    AnswerAttributesMeta answerAttributes;
    AnswerCustomDataMeta answerHeroCustomDataRequirement;
    AnswerCustomDataMeta answerGlobalCustomDataRequirement;

    // --- answer results

    AnswerBurnRandomItemMeta answerBurnRandomItemMeta;
    NextObjRewriteMeta nextObjRewriteMeta;

    // --- story results

    AnswerResultMeta successInfo;
    AnswerResultMeta failInfo;

    AnswerCustomDataResultMeta successHeroCustomData;
    AnswerCustomDataResultMeta failHeroCustomData;
    AnswerCustomDataResultMeta successGlobalCustomData;
    AnswerCustomDataResultMeta failGlobalCustomData;
  }

  struct NextObjRewriteMeta {
    uint16[] nextObjPageIds;
    uint8[] nextObjHeroClasses;
    uint32[][] nextObjIds;
  }

  struct AnswersMeta {
    uint16[] answerPageIds;
    uint8[] answerHeroClasses;
    uint16[] answerIds;
  }

  struct AnswerNextPageMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;
    uint8[] answerResultIds;
    uint16[][] answerNextPageIds;
  }

  struct AnswerAttributeRequirementsMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;
    bool[][] cores;
    uint8[][] ids;
    int32[][] values;
  }

  struct AnswerItemRequirementsMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;
    address[][] requireItems;
    bool[][] requireItemBurn;
    bool[][] requireItemEquipped;
  }

  struct AnswerTokenRequirementsMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;
    address[][] requireToken;
    uint88[][] requireAmount;
    bool[][] requireTransfer;
  }

  struct AnswerAttributesMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;
    uint32[] randomRequirements;
    uint32[] delayRequirements;
    bool[] isFinalAnswer;
  }

  struct AnswerCustomDataMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;

    bytes32[][] dataIndexes;
    bool[][] mandatory;
    uint64[][] dataValuesMin;
    uint64[][] dataValuesMax;
  }

  struct AnswerResultMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;

    uint8[][] attributeIds;
    /// @dev Max value is limitied by int24, see toBytes32ArrayWithIds impl
    int32[][] attributeValues;

    uint32[] experience;
    int32[] heal;
    int32[] manaRegen;
    int32[] lifeChancesRecovered;
    int32[] damage;
    int32[] manaConsumed;

    address[][] mintItems;
    uint32[][] mintItemsChances;
  }

  struct AnswerCustomDataResultMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;

    bytes32[][] dataIndexes;
    int16[][] dataValues;
  }

  struct AnswerBurnRandomItemMeta {
    uint16[] pageId;
    uint8[] heroClass;
    uint16[] answerId;

    /// @notice 0 - random slot
    uint8[][] slots;
    /// @notice typical chances are [0..100] (no decimals here)
    uint64[][] chances;
    /// @notice Since SIP-003 the burning is replaced by breaking bu the name is kept as is
    bool[][] isStopIfBurnt;
  }

  struct CustomDataRequirementPacked {
    bytes32 index;
    /// @dev min(uint64) + max(uint64) + mandatory(uint8)
    bytes32 data;
  }

  struct CustomDataRequirementRangePacked {
    bytes32 index;
    /// @dev min(uint64) + max(uint64) + isHeroData(uint8)
    bytes32 data;
  }

  struct StatsChange {
    uint32 experience;
    int32 heal;
    int32 manaRegen;
    int32 lifeChancesRecovered;
    int32 damage;
    int32 manaConsumed;
  }

  struct StoryActionContext {
    uint stageId;
    uint iteration;
    bytes32 answerIdHash;
    bytes32 answerAttributes;
    address sender;
    address heroToken;
    IController controller;
    IStatController statController;
    IHeroController heroController;
    IOracle oracle;
    IItemController itemController;
    uint8 heroClass;
    uint8 heroClassFromAnswerHash;
    uint8 biome;
    uint16 storyId;
    uint16 storyIdFromAnswerHash;
    uint16 pageIdFromAnswerHash;
    uint16 answerNumber;
    uint16 pageId;
    uint32 objectId;
    uint64 dungeonId;
    uint40 heroLastActionTS;
    uint80 heroTokenId;
    IStatController.ChangeableStats heroStats;
  }

  // --- WRITE ---

  function storyAction(
    address sender,
    uint64 dungeonId,
    uint32 objectId,
    uint stageId,
    address heroToken,
    uint heroTokenId,
    uint8 biome,
    uint iteration,
    bytes memory data
  ) external returns (IGOC.ActionResult memory);

  // --- READ ---

  function isStoryAvailableForHero(uint32 objectId, address heroToken, uint heroTokenId) external view returns (bool);

  function idToStory(uint16 id) external view returns (uint32 objectId);

  function heroPage(address hero, uint80 heroId, uint16 storyId) external view returns (uint16 pageId);

  function storyIds(uint32 objectId) external view returns (uint16);

  function registeredStories(uint32 objectId) external view returns (bool);

}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

import "./IItemController.sol";

interface ITreasury {

  function balanceOfToken(address token) external view returns (uint);

  function sendToDungeon(address dungeon, address token, uint amount) external;
}

// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.23;

interface IUserController {

  //region ------------------------ Data types

  enum LootBoxKind {
    /// @notice small loot box - reward for the daily activity
    DAILY_0,
    /// @notice large loot box - reward for the weekly activity (daily activity is passed each ot of the 7 days)
    WEEKLY_1,

    END_SLOT
  }

  /// @dev registerPassedDungeon assumes that the whole struct takes single slot only, not more
  struct UserActivity {
    /// @notice A day for which the daily activity is calculated (see counterXXX below)
    /// The number of days since 1970-01-01
    uint32 epochDay;

    /// @notice A week for which total count of daily activities were calculated
    /// The number of weeks since (1970-01-01 Thursday) - 3 days = (1969-12-29 Monday)
    uint32 epochWeek;

    /// @notice Count of dungeons passed during the day
    uint32 counterPassedDungeons;
    /// @notice Count of PvP during the day
    uint32 counterPvp;

    /// @notice Count of daily activities completed per the week
    uint16 dailyActivities;

    /// @notice Daily activity is completed and small loot box is added to the earned loot boxes
    bool dailyLootBoxReceived;
    /// @notice Weekly activity is completed and large loot box is added to the earned loot boxes
    bool weeklyLootBoxReceived;
  }

  struct EarnedLootBoxes {
    /// @notice Count of loot boxes earned by daily activity
    uint32 dailyCounter;
    /// @notice Count of loot boxes earned by weekly activity
    uint32 weeklyCounter;
  }

  struct LootBoxConfig {
    address[] mintItems;
    uint32[] mintItemsChances;
    uint maxDropItems;
  }

  /// @custom:storage-location erc7201:user.controller.main
  struct MainState {
    /// @notice Amount of sacra required to rename user account
    uint feeRenaming;

    /// @dev user EOA => account name
    mapping(address => string) userAccountName;

    /// @dev name => user EOA, needs for checking uniq names
    mapping(string => address) nameToUserAccount;

    /// @notice user => daily activity info
    mapping(address => UserActivity) userActivity;

    /// @notice user => earned loot boxes
    mapping(address => EarnedLootBoxes) counterLootBoxes;

    /// @notice Configs of loot boxes of various kinds
    mapping(LootBoxKind => LootBoxConfig) lootBoxConfig;

    /// @dev Deprecated, controller is used instead.
    address userTokensVault;

    /// @dev user EOA => account avatar
    mapping(address => string) userAvatar;

    // @notice Hall of Fame: ngLevel [1...99] => who opened the NG_LEVEL first
    mapping(uint8 ngLevel => FameHallData) fameHall;
  }

  struct FameHallData {
    // ------------ slot 1
    /// @notice The hero who opened given the NG_LEVEL first
    address hero;
    uint64 heroId;
    // ------------ slot 2
    /// @notice The owner of the hero
    address heroOwner;
    /// @notice Timestamp of the moment of the opening given NG_LEVEL
    uint64 tsOpen;
  }

  //endregion ------------------------ Data types

  /// @notice Register daily activity - a dungeon was passed
  /// @param user Owner of the hero who has passed the dungeon
  function registerPassedDungeon(address user) external;

  /// @notice Register daily activity - PvP was made
  /// @param user Owner of the hero who has taken participation in the PvP
  function registerPvP(address user, bool isWinner) external;

  function registerFameHallHero(address hero, uint heroId, uint8 openedNgLevel) external;
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IAppErrors.sol";
import "../solady/LibPRNG.sol";

library CalcLib {

  uint32 public constant MAX_CHANCE = 1e9;

  function minI32(int32 a, int32 b) internal pure returns (int32) {
    return a < b ? a : b;
  }

  function max32(int32 a, int32 b) internal pure returns (int32) {
    return a >= b ? a : b;
  }

  function absDiff(int32 a, int32 b) internal pure returns (uint32) {
    if (!((a >= 0 && b >= 0) || (a <= 0 && b <= 0))) revert IAppErrors.AbsDiff(a, b);
    if (a < 0) {
      a = - a;
    }
    if (b < 0) {
      b = - b;
    }
    return uint32(uint(int(a >= b ? a - b : b - a)));
  }

  function toUint(int32 n) internal pure returns (uint) {
    if (n <= 0) {
      return 0;
    }
    return uint(int(n));
  }

  function toInt32(uint a) internal pure returns (int32){
    if (a >= uint(int(type(int32).max))) {
      return type(int32).max;
    }
    return int32(int(a));
  }

  /// @dev Simplified pseudo-random for minor functionality
  function pseudoRandom(uint maxValue) internal view returns (uint) {
    if (maxValue == 0) {
      return 0;
    }

    uint salt = genSalt();
    // pseudo random number
    return (uint(keccak256(abi.encodePacked(blockhash(block.number), block.coinbase, block.difficulty, block.number, block.timestamp, tx.gasprice, gasleft(), salt))) % (maxValue + 1));
  }

  function genSalt() internal view returns (uint salt) {
    // skale has a RNG Endpoint
    if (
      block.chainid == uint(1351057110)
      || block.chainid == uint(37084624)
    ) {
      assembly {
        let freemem := mload(0x40)
        let start_addr := add(freemem, 0)
        if iszero(staticcall(gas(), 0x18, 0, 0, start_addr, 32)) {
          invalid()
        }
        salt := mload(freemem)
      }
    }
  }

  function pseudoRandomUint32(uint32 maxValue) internal view returns (uint32) {
    return uint32(pseudoRandom(uint(maxValue)));
  }

  /// @notice Generate pseudo-random uint in the range [0..maxValue) using Solady pseudo-random function
  function nextPrng(LibPRNG.PRNG memory prng, uint maxValue) internal pure returns (uint) {
    return LibPRNG.next(prng) % maxValue;
  }

  /// @notice pseudoRandomUint32 with customizable pseudoRandom()
  function pseudoRandomUint32Flex(
    uint32 maxValue,
    function (uint) internal view returns (uint) random_
  ) internal view returns (uint32) {
    return uint32(random_(uint(maxValue)));
  }

  function pseudoRandomInt32(int32 maxValue) internal view returns (int32) {
    bool neg;
    if (maxValue < 0) {
      neg = true;
      maxValue = - maxValue;
    }
    uint32 v = uint32(pseudoRandom(uint(int(maxValue))));
    return neg
      ? - int32(int(uint(v)))
      : int32(int(uint(v)));
  }

  /// @dev Simplified pseudo-random for minor functionality
  function pseudoRandomWithSeed(uint maxValue, uint seed) internal view returns (uint) {
    if (maxValue == 0) {
      return 0;
    }
    uint salt = genSalt();
    // pseudo random number
    return (uint(keccak256(abi.encodePacked(blockhash(block.number), block.coinbase, block.difficulty, block.number, block.timestamp, tx.gasprice, gasleft(), seed, salt))) % (maxValue + 1));
  }

  /// @dev Simplified pseudo-random for minor functionality, in range
  function pseudoRandomInRange(uint min, uint max) internal view returns (uint) {
    if (min >= max) {
      return max;
    }
    uint r = pseudoRandom(max - min);
    return min + r;
  }

  /// @dev Simplified pseudo-random for minor functionality, in range
  ///      Equal to pseudoRandomInRange(min, max, pseudoRandom)
  function pseudoRandomInRangeFlex(
    uint min,
    uint max,
    function (uint) internal view returns (uint) random_
  ) internal view returns (uint) {
    return min >= max ? max : min + random_(max - min);
  }

  function minusWithZeroFloor(uint a, uint b) internal pure returns (uint){
    if (a <= b) {
      return 0;
    }
    return a - b;
  }

  function minusWithMinFloorI32(int32 a, int32 b) internal pure returns (int32){
    if (int(a) - int(b) < type(int32).min) {
      return type(int32).min;
    }
    return a - b;
  }

  function plusWithMaxFloor32(int32 a, int32 b) internal pure returns (int32){
    if (int(a) + int(b) >= type(int32).max) {
      return type(int32).max;
    }
    return a + b;
  }

  function sqrt(uint x) internal pure returns (uint z) {
    assembly {
    // Start off with z at 1.
      z := 1

    // Used below to help find a nearby power of 2.
      let y := x

    // Find the lowest power of 2 that is at least sqrt(x).
      if iszero(lt(y, 0x100000000000000000000000000000000)) {
        y := shr(128, y) // Like dividing by 2 ** 128.
        z := shl(64, z) // Like multiplying by 2 ** 64.
      }
      if iszero(lt(y, 0x10000000000000000)) {
        y := shr(64, y) // Like dividing by 2 ** 64.
        z := shl(32, z) // Like multiplying by 2 ** 32.
      }
      if iszero(lt(y, 0x100000000)) {
        y := shr(32, y) // Like dividing by 2 ** 32.
        z := shl(16, z) // Like multiplying by 2 ** 16.
      }
      if iszero(lt(y, 0x10000)) {
        y := shr(16, y) // Like dividing by 2 ** 16.
        z := shl(8, z) // Like multiplying by 2 ** 8.
      }
      if iszero(lt(y, 0x100)) {
        y := shr(8, y) // Like dividing by 2 ** 8.
        z := shl(4, z) // Like multiplying by 2 ** 4.
      }
      if iszero(lt(y, 0x10)) {
        y := shr(4, y) // Like dividing by 2 ** 4.
        z := shl(2, z) // Like multiplying by 2 ** 2.
      }
      if iszero(lt(y, 0x8)) {
      // Equivalent to 2 ** z.
        z := shl(1, z)
      }

    // Shifting right by 1 is like dividing by 2.
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))
      z := shr(1, add(z, div(x, z)))

    // Compute a rounded down version of z.
      let zRoundDown := div(x, z)

    // If zRoundDown is smaller, use it.
      if lt(zRoundDown, z) {
        z := zRoundDown
      }
    }
  }

  /*********************************************
 *              PRB-MATH                      *
 *   https://github.com/hifi-finance/prb-math *
 **********************************************/
  /// @notice Calculates the binary logarithm of x.
  ///
  /// @dev Based on the iterative approximation algorithm.
  /// https://en.wikipedia.org/wiki/Binary_logarithm#Iterative_approximation
  ///
  /// Requirements:
  /// - x must be greater than or equal to SCALE, otherwise the result would be negative.
  ///
  /// Caveats:
  /// - The results are nor perfectly accurate to the last decimal,
  ///   due to the lossy precision of the iterative approximation.
  ///
  /// @param x The unsigned 60.18-decimal fixed-point number for which
  ///           to calculate the binary logarithm.
  /// @return result The binary logarithm as an unsigned 60.18-decimal fixed-point number.
  function log2(uint256 x) internal pure returns (uint256 result) {
    if (x < 1e18) revert IAppErrors.TooLowX(x);

    // Calculate the integer part of the logarithm
    // and add it to the result and finally calculate y = x * 2^(-n).
    uint256 n = mostSignificantBit(x / 1e18);

    // The integer part of the logarithm as an unsigned 60.18-decimal fixed-point number.
    // The operation can't overflow because n is maximum 255 and SCALE is 1e18.
    uint256 rValue = n * 1e18;

    // This is y = x * 2^(-n).
    uint256 y = x >> n;

    // If y = 1, the fractional part is zero.
    if (y == 1e18) {
      return rValue;
    }

    // Calculate the fractional part via the iterative approximation.
    // The "delta >>= 1" part is equivalent to "delta /= 2", but shifting bits is faster.
    for (uint256 delta = 5e17; delta > 0; delta >>= 1) {
      y = (y * y) / 1e18;

      // Is y^2 > 2 and so in the range [2,4)?
      if (y >= 2 * 1e18) {
        // Add the 2^(-m) factor to the logarithm.
        rValue += delta;

        // Corresponds to z/2 on Wikipedia.
        y >>= 1;
      }
    }
    return rValue;
  }

  /// @notice Finds the zero-based index of the first one in the binary representation of x.
  /// @dev See the note on msb in the "Find First Set"
  ///      Wikipedia article https://en.wikipedia.org/wiki/Find_first_set
  /// @param x The uint256 number for which to find the index of the most significant bit.
  /// @return msb The index of the most significant bit as an uint256.
  //noinspection NoReturn
  function mostSignificantBit(uint256 x) internal pure returns (uint256 msb) {
    if (x >= 2 ** 128) {
      x >>= 128;
      msb += 128;
    }
    if (x >= 2 ** 64) {
      x >>= 64;
      msb += 64;
    }
    if (x >= 2 ** 32) {
      x >>= 32;
      msb += 32;
    }
    if (x >= 2 ** 16) {
      x >>= 16;
      msb += 16;
    }
    if (x >= 2 ** 8) {
      x >>= 8;
      msb += 8;
    }
    if (x >= 2 ** 4) {
      x >>= 4;
      msb += 4;
    }
    if (x >= 2 ** 2) {
      x >>= 2;
      msb += 2;
    }
    if (x >= 2 ** 1) {
      // No need to shift x any more.
      msb += 1;
    }
  }

}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IController.sol";
import "../interfaces/IOracle.sol";
import "../interfaces/IStatController.sol";
import "../interfaces/IStoryController.sol";
import "../interfaces/ITreasury.sol";
import "../interfaces/IDungeonFactory.sol";
import "../interfaces/IReinforcementController.sol";
import "../interfaces/IGameToken.sol";
import "../interfaces/IGOC.sol";
import "../interfaces/IItemController.sol";
import "../interfaces/IHeroController.sol";
import "../interfaces/IUserController.sol";
import "../interfaces/IGuildController.sol";
import "../interfaces/IRewardsPool.sol";

/// @notice Provide context-struct with all controller addresses and routines for lazy init
/// Usage:
///       Create an instance of the structure
///               cc = ControllerContextLib.init(controller);
///       access controller directly
///               cc.controller.xxx();
///       access other contracts indirectly
///               sc = ControllerContextLib.getStatController(cc);
library ControllerContextLib {
  struct ControllerContext {
    IController controller;
    IStatController statController;
    IStoryController storyController;
    IOracle oracle;
    ITreasury treasury;
    IDungeonFactory dungeonFactory;
    IGOC gameObjectController;
    IReinforcementController reinforcementController;
    IItemController itemController;
    IHeroController heroController;
    IGameToken gameToken;
    IUserController userController;
    IGuildController guildController;
    IRewardsPool rewardsPool;
  }

  function init(IController controller) internal pure returns (ControllerContext memory cc) {
    cc.controller = controller;
    return cc;
  }

  function getStatController(ControllerContext memory cc) internal view returns (IStatController statController) {
    if (address(cc.statController) == address(0)) {
      cc.statController = IStatController(cc.controller.statController());
    }
    return cc.statController;
  }

  function getStoryController(ControllerContext memory cc) internal view returns (IStoryController storyController) {
    if (address(cc.storyController) == address(0)) {
      cc.storyController = IStoryController(cc.controller.storyController());
    }
    return cc.storyController;
  }

  function getOracle(ControllerContext memory cc) internal view returns (IOracle oracle) {
    if (address(cc.oracle) == address(0)) {
      cc.oracle = IOracle(cc.controller.oracle());
    }
    return cc.oracle;
  }

  function getTreasury(ControllerContext memory cc) internal view returns (ITreasury treasury) {
    if (address(cc.treasury) == address(0)) {
      cc.treasury = ITreasury(cc.controller.treasury());
    }
    return cc.treasury;
  }

  function getDungeonFactory(ControllerContext memory cc) internal view returns (IDungeonFactory dungeonFactory) {
    if (address(cc.dungeonFactory) == address(0)) {
      cc.dungeonFactory = IDungeonFactory(cc.controller.dungeonFactory());
    }
    return cc.dungeonFactory;
  }

  function getGameObjectController(ControllerContext memory cc) internal view returns (IGOC gameObjectController) {
    if (address(cc.gameObjectController) == address(0)) {
      cc.gameObjectController = IGOC(cc.controller.gameObjectController());
    }
    return cc.gameObjectController;
  }

  function getReinforcementController(ControllerContext memory cc) internal view returns (IReinforcementController reinforcementController) {
    if (address(cc.reinforcementController) == address(0)) {
      cc.reinforcementController = IReinforcementController(cc.controller.reinforcementController());
    }
    return cc.reinforcementController;
  }

  function getItemController(ControllerContext memory cc) internal view returns (IItemController itemController) {
    if (address(cc.itemController) == address(0)) {
      cc.itemController = IItemController(cc.controller.itemController());
    }
    return cc.itemController;
  }

  function getHeroController(ControllerContext memory cc) internal view returns (IHeroController heroController) {
    if (address(cc.heroController) == address(0)) {
      cc.heroController = IHeroController(cc.controller.heroController());
    }
    return cc.heroController;
  }

  function getGameToken(ControllerContext memory cc) internal view returns (IGameToken gameToken) {
    if (address(cc.gameToken) == address(0)) {
      cc.gameToken = IGameToken(cc.controller.gameToken());
    }
    return cc.gameToken;
  }

  function getUserController(ControllerContext memory cc) internal view returns (IUserController userController) {
    if (address(cc.userController) == address(0)) {
      cc.userController = IUserController(cc.controller.userController());
    }
    return cc.userController;
  }

  function getGuildController(ControllerContext memory cc) internal view returns (IGuildController guildController) {
    if (address(cc.guildController) == address(0)) {
      cc.guildController = IGuildController(cc.controller.guildController());
    }
    return cc.guildController;
  }

  function getRewardsPool(ControllerContext memory cc) internal view returns (IRewardsPool rewardsPool) {
    if (address(cc.rewardsPool) == address(0)) {
      cc.rewardsPool = IRewardsPool(cc.controller.rewardsPool());
    }
    return cc.rewardsPool;
  }
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "./CalcLib.sol";
import "./PackingLib.sol";
import "./ItemLib.sol";
import "./StoryLib.sol";
import "../interfaces/IStatController.sol";
import "../interfaces/IOracle.sol";
import "../interfaces/IGOC.sol";
import "../interfaces/IAppErrors.sol";
import "../interfaces/IApplicationEvents.sol";

library EventLib {
  using CalcLib for int32;
  using PackingLib for bytes32;
  using PackingLib for bytes32[];
  using PackingLib for uint16;
  using PackingLib for uint8;
  using PackingLib for address;
  using PackingLib for uint32[];
  using PackingLib for uint32;
  using PackingLib for uint64;
  using PackingLib for int32[];
  using PackingLib for int32;

  //region ------------------------ Main logic

  function action(IGOC.ActionContext calldata ctx, IGOC.EventInfo storage info) external returns (
    IGOC.ActionResult memory
  ) {
    (bool accept) = abi.decode(ctx.data, (bool));
    return accept
      ? _eventAcceptResult(ctx, info)
      : _noActionResult();
  }

  /// @notice Save data from {regInfo} to {info}
  function eventRegInfoToInfo(IGOC.EventRegInfo calldata regInfo, IGOC.EventInfo storage info) external {
    info.goodChance = regInfo.goodChance;
    info.goodAttributes = regInfo.goodAttributes.values.toBytes32ArrayWithIds(regInfo.goodAttributes.ids);
    info.badAttributes = regInfo.badAttributes.values.toBytes32ArrayWithIds(regInfo.badAttributes.ids);
    info.statsChange = regInfo.experience.packStatsChange(
      regInfo.heal,
      regInfo.manaRegen,
      regInfo.lifeChancesRecovered,
      regInfo.damage,
      regInfo.manaConsumed
    );

    bytes32[] memory mintItems = new bytes32[](regInfo.mintItems.length);

    for (uint i; i < mintItems.length; ++i) {
      mintItems[i] = regInfo.mintItems[i].packItemMintInfo(regInfo.mintItemsChances[i]);
    }
    info.mintItems = mintItems;
  }
  //endregion ------------------------ Main logic

  //region ------------------------ Internal logic
  function _eventAcceptResult(IGOC.ActionContext calldata ctx, IGOC.EventInfo storage info) internal returns (
    IGOC.ActionResult memory result
  ) {
    IStatController sc = IStatController(ctx.controller.statController());

    IStatController.ActionInternalInfo memory gen = _generate(ctx, info, sc);

    if (gen.posAttributes.length != 0) {
      sc.changeBonusAttributes(IStatController.ChangeAttributesInfo({
        heroToken: ctx.heroToken,
        heroTokenId: ctx.heroTokenId,
        changeAttributes: gen.posAttributes,
        add: true,
        temporally: true
      }));
    }

    if (gen.negAttributes.length != 0) {
      sc.changeBonusAttributes(IStatController.ChangeAttributesInfo({
        heroToken: ctx.heroToken,
        heroTokenId: ctx.heroTokenId,
        changeAttributes: gen.negAttributes,
        add: true,
        temporally: true
      }));
    }

    // refreshed stats
    IStatController.ChangeableStats memory stats = sc.heroStats(ctx.heroToken, ctx.heroTokenId);

    result.completed = true;
    result.experience = gen.experience;
    result.heal = gen.heal;
    result.manaRegen = gen.manaRegen;
    result.lifeChancesRecovered = gen.lifeChancesRecovered;
    result.damage = gen.damage;
    result.manaConsumed = CalcLib.minI32(gen.manaConsumed, int32(stats.mana));
    result.mintItems = gen.mintedItems;

    if (stats.life <= gen.damage.toUint()) {
      result.kill = true;
    }

    emit IApplicationEvents.EventResult(ctx.dungeonId, ctx.heroToken, ctx.heroTokenId, ctx.stageId, gen, ctx.iteration);
    return result;
  }

  /// @notice Generate empty result structure, only "completed" is true
  function _noActionResult() internal pure returns (IGOC.ActionResult memory result) {
    result.completed = true;
    return result;
  }

  /// @notice Generate either positive or negative attributes, mint single item in any case
  function _generate(IGOC.ActionContext calldata ctx, IGOC.EventInfo storage info, IStatController sc) internal returns (
    IStatController.ActionInternalInfo memory result
  ) {
    uint32 goodChance = info.goodChance;
    if (goodChance > CalcLib.MAX_CHANCE) revert IAppErrors.TooHighChance(goodChance);

    IOracle oracle = IOracle(ctx.controller.oracle());

    uint random = goodChance == CalcLib.MAX_CHANCE ? CalcLib.MAX_CHANCE : oracle.getRandomNumber(CalcLib.MAX_CHANCE, 0);
    if (random <= goodChance) {
      result.posAttributes = StoryLib._generateAttributes(info.goodAttributes);
      (result.experience,
        result.heal,
        result.manaRegen,
        result.lifeChancesRecovered,,) = info.statsChange.unpackStatsChange();
    } else {
      result.negAttributes = StoryLib._generateAttributes(info.badAttributes);
      (,,,, result.damage, result.manaConsumed) = info.statsChange.unpackStatsChange();
    }

    // always mint possible items even if bad result
    result.mintedItems = _mintRandomItem(ctx, info, oracle, sc, CalcLib.nextPrng);

    return result;
  }

  /// @notice Mint single random item
  /// @param nextPrng_ CalcLib.nextPrng, param is required by unit tests
  function _mintRandomItem(
    IGOC.ActionContext calldata ctx,
    IGOC.EventInfo storage info,
    IOracle oracle,
    IStatController sc,
    function (LibPRNG.PRNG memory, uint) internal view returns (uint) nextPrng_
  ) internal returns (address[] memory minted) {
    bytes32[] memory mintItemsPacked = info.mintItems;
    if (mintItemsPacked.length == 0) {
      return minted;
    }

    IStatController.ChangeableStats memory stats = sc.heroStats(ctx.heroToken, ctx.heroTokenId);

    address[] memory mintItems = new address[](mintItemsPacked.length);
    uint32[] memory mintItemsChances = new uint32[](mintItemsPacked.length);

    for (uint i = 0; i < mintItemsPacked.length; i++) {
      (mintItems[i], mintItemsChances[i]) = mintItemsPacked[i].unpackItemMintInfo();
    }

    return ItemLib._mintRandomItems(
      ItemLib.MintItemInfo({
        mintItems: mintItems,
        mintItemsChances: mintItemsChances,
        amplifier: 0,
        seed: 0,
        oracle: oracle,
        magicFind: 0,
        destroyItems: 0,
        maxItems: 1, // MINT ONLY 1 ITEM!
        mintDropChanceDelta: StatLib.mintDropChanceDelta(stats.experience, uint8(stats.level), ctx.biome),
        mintDropChanceNgLevelMultiplier: 1e18
      }),
      nextPrng_
    );
  }
  //endregion ------------------------ Internal logic
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IAppErrors.sol";
import "../interfaces/IApplicationEvents.sol";
import "../interfaces/IGuildController.sol";
import "../interfaces/IItem.sol";
import "../interfaces/IItemController.sol";
import "../interfaces/IOracle.sol";
import "../openzeppelin/Math.sol";
import "../solady/LibPRNG.sol";
import "./CalcLib.sol";
import "./ControllerContextLib.sol";
import "./ShelterLib.sol";
import "./StatLib.sol";

library ItemLib {
  using CalcLib for int32;
  using PackingLib for address;
  using PackingLib for bytes32;
  using PackingLib for bytes32[];
  using PackingLib for uint32[];
  using PackingLib for int32[];

  //region ------------------------ Data types
  struct GenerateAttributesContext {
    /// @notice True if max allowed amount of random attributes were reached inside {_prepareAttributes}
    bool stopGenerateRandom;
    /// @notice Flag - attribute was generated. The array matches to info.ids
    bool[] usedIndexes;
    /// @notice Ids of the generated attributes
    uint8[] ids;
    /// @notice Randomly selected values of the generated attributes
    int32[] values;
    /// @notice Counter of the stored values into {ids} and {values}
    uint counter;
    /// @notice Total number of random attributes that were generated inside {_prepareAttributes}
    uint randomAttrCounter;
    /// @notice Total sum of all {random} values for random attributes generated in {_prepareAttributes}
    uint randomSum;
    /// @notice Total sum of all chances of the random attributes generated in {_prepareAttributes}
    uint chancesSum;
    /// @notice Total number of random attributes that can be generated
    uint totalRandomAttrsPossible;
  }

  struct MintItemInfo {
    uint8 maxItems;
    int32 magicFind;
    int32 destroyItems;
    uint32[] mintItemsChances;
    IOracle oracle;
    address[] mintItems;
    uint amplifier;
    uint seed;
    /// @notice Penalty to reduce chance as chance/delta if the hero not in his biome
    /// @dev Use StatLib.mintDropChanceDelta
    uint mintDropChanceDelta;
    /// @notice SCR-1064: drop chance depends on NG_LEVEL, decimals 18, value is in the range [0...1e18]
    /// it's always 100% for NG0 (no reduce, value is 1e18)
    /// Use {dropChancePercent} to calculate actual value
    uint mintDropChanceNgLevelMultiplier;
  }
  //endregion ------------------------ Data types

  //region ------------------------ Restrictions
  /// @notice ensure that the user belongs to a guild, the guild has a shelter, the shelter has highest level 3
  function _onlyMemberOfGuildWithShelterMaxLevel(IController controller, address msgSender) internal view {
    // ensure that signer belongs to a guild and the guild has a shelter of ANY level
    IGuildController gc = IGuildController(controller.guildController());

    uint guildId = gc.memberOf(msgSender);
    if (guildId == 0) revert IAppErrors.NotGuildMember();

    uint shelterId = gc.guildToShelter(guildId);
    if (shelterId == 0) revert IAppErrors.GuildHasNoShelter();

    // only highest level of shelters gives possibility to exit from dungeon
    (, uint8 shelterLevel,) = PackingLib.unpackShelterId(shelterId);
    if (shelterLevel != ShelterLib.MAX_SHELTER_LEVEL) revert IAppErrors.TooLowShelterLevel(shelterLevel, ShelterLib.MAX_SHELTER_LEVEL);
  }

  //endregion ------------------------ Restrictions

  //region ------------------------ Main logic

  /// @notice Mint new item, setup attributes, make extra setup if necessary (setup attack item, buff item)
  /// @param sender Dungeon Factory / User Controller / Guild Controller are allowed
  /// @param item Item to be minted
  /// @param recipient The item is minted for the given recipient
  /// @return itemId Id of the newly minted item
  function mintNewItem(
    IItemController.MainState storage s,
    IController controller,
    address sender,
    address item,
    address recipient
  ) external returns (uint itemId) {
    ControllerContextLib.ControllerContext memory ctx = ControllerContextLib.init(controller);

    address guildController = address(ControllerContextLib.getGuildController(ctx));
    address shelterController = guildController == address(0)
      ? address(0)
      : IGuildController(guildController).shelterController();

    if (
      address(ControllerContextLib.getDungeonFactory(ctx)) != sender
      && address(ControllerContextLib.getUserController(ctx)) != sender
      && guildController != sender
      && shelterController != sender
      && address(ControllerContextLib.getItemController(ctx)) != sender
      && address(ControllerContextLib.getHeroController(ctx)) != sender
    ) revert IAppErrors.MintNotAllowed();

    itemId = IItem(item).mintFor(recipient);

    IItemController.MintInfo memory info;

    (
      info.meta,
      info.attributesIds,
      info.attributesValues,
      info.itemRarity
    ) = _setupNewAttributes(s, item, itemId, CalcLib.pseudoRandom);

    // setup extra info

    if (info.meta.itemMetaType == uint8(IItemController.ItemMetaType.ATTACK)) {
      info.attackInfo = unpackItemAttackInfo(_setupNewAttackItem(s, item, itemId));
    } else if (info.meta.itemMetaType == uint8(IItemController.ItemMetaType.BUFF)) {
      (
        info.casterIds,
        info.casterValues,
        info.targetIds,
        info.targetValues
      ) = _setupNewBuffItem(s, item, itemId, CalcLib.pseudoRandom);
    }
    // consumable stats unchangeable, get them by address

    emit IApplicationEvents.NewItemMinted(item, itemId, info);
  }

  /// @notice Mint random items, not more than {info.maxItems}
  function mintRandomItems(MintItemInfo memory info) internal returns (address[] memory) {
    return _mintRandomItems(info, CalcLib.nextPrng);
  }

  function applyActionMasks(
    uint actionMask,
    IStatController statController,
    address heroToken,
    uint heroTokenId
  ) external {
    if ((actionMask & (2 ** uint(IItemController.ConsumableActionBits.CLEAR_TEMPORARY_ATTRIBUTES_0))) != 0) {
      statController.clearTemporallyAttributes(heroToken, heroTokenId);
    }
  }

  //endregion ------------------------ Main logic

  //region ------------------------ Internal logic
  /// @param nextPrng_ CalcLib.nextPrng, param is required by unit tests
  function _mintRandomItems(
    MintItemInfo memory info,
    function (LibPRNG.PRNG memory, uint) internal view returns (uint) nextPrng_
  ) internal returns (address[] memory) {

    // if hero is not in his biome do not mint at all
    if (info.mintDropChanceDelta != 0) {
      return new address[](0);
    }

    uint len = info.mintItems.length;

    // Fisher–Yates shuffle
    LibPRNG.PRNG memory prng = LibPRNG.PRNG(info.oracle.getRandomNumber(CalcLib.MAX_CHANCE, info.seed));
    uint[] memory indices = new uint[](len);
    for (uint i = 1; i < len; ++i) {
      indices[i] = i;
    }
    LibPRNG.shuffle(prng, indices);

    address[] memory minted = new address[](len);
    uint mintedLength;
    uint di = Math.min(CalcLib.toUint(info.destroyItems), 100);

    for (uint i; i < len; ++i) {
      if (info.mintItemsChances[indices[i]] > CalcLib.MAX_CHANCE) {
        revert IAppErrors.TooHighChance(info.mintItemsChances[indices[i]]);
      }

      uint chance = _adjustChance(info.mintItemsChances[indices[i]], info, di);

      // need to call random in each loop coz each minted item should have dedicated chance
      uint rnd = nextPrng_(prng, CalcLib.MAX_CHANCE); // randomWithSeed_(CalcLib.MAX_CHANCE, rndSeed);

      if (chance != 0 && (chance >= CalcLib.MAX_CHANCE || rnd < chance)) {
        // There is no break here: the cycle is continued even if the number of the minted items reaches the max.
        // The reason: gas consumption of success operation must be great of equal of the gas consumption of fail op.
        if (mintedLength < info.maxItems) {
          minted[i] = info.mintItems[indices[i]];
          ++mintedLength;
        }
      }
    }

    address[] memory mintedAdjusted = new address[](mintedLength);
    uint j;
    for (uint i; i < len; ++i) {
      if (minted[i] != address(0)) {
        mintedAdjusted[j] = minted[i];
        ++j;
      }
    }

    return mintedAdjusted;
  }

  /// @notice Apply all corrections to the chance of item drop
  /// There are two params to increase chances: amplifier and magicFind
  /// There are two params to decrease chances: destroyItems and mintDropChanceNgLevelMultiplier
  /// @param info Assume here, that info.mintDropChanceNgLevelMultiplier is in the range [0..1e18]
  /// @param di Assume that di <= 100
  function _adjustChance(uint32 itemChance, MintItemInfo memory info, uint di) internal pure returns (uint) {
    uint chance = uint(itemChance) * Math.min(1e18, info.mintDropChanceNgLevelMultiplier) / 1e18;
    chance += chance * info.amplifier / StatLib._MAX_AMPLIFIER;
    chance += chance * CalcLib.toUint(info.magicFind) / 100;
    chance -= chance * di / 100;
    return chance;
  }

  function _setupNewAttributes(
    IItemController.MainState storage s,
    address item,
    uint itemId,
    function (uint) internal view returns (uint) random_
  ) internal returns (
    IItemController.ItemMeta memory meta,
    uint8[] memory ids,
    int32[] memory values,
    IItemController.ItemRarity itemRarity
  ){
    meta = unpackedItemMeta(s.itemMeta[item]);
    (ids, values, itemRarity) = _generateAttributes(unpackItemGenerateInfo(s.generateInfoAttributes[item]), meta, random_);

    bytes32 packedItemId = item.packNftId(itemId);
    if (ids.length != 0) {
      s._itemAttributes[packedItemId] = values.toBytes32ArrayWithIds(ids);
    }

    s.itemInfo[packedItemId] = PackingLib.packItemInfo(uint8(itemRarity), 0, meta.baseDurability);
  }

  function _setupNewAttackItem(IItemController.MainState storage s, address item, uint itemId) internal returns (bytes32 attackInfo){
    // we just write data for attack item, no need to generate, it will be augmented later so need individual data for itemId
    attackInfo = s.generateInfoAttack[item];
    s._itemAttackInfo[item.packNftId(itemId)] = attackInfo;
  }

  function _setupNewBuffItem(
    IItemController.MainState storage s,
    address item,
    uint itemId,
    function (uint) internal view returns (uint) random_
  ) internal returns (
    uint8[] memory casterIds,
    int32[] memory casterValues,
    uint8[] memory targetIds,
    int32[] memory targetValues
  ){

    // CASTER
    (casterIds, casterValues) = _generateSimpleAttributes(
      unpackItemGenerateInfo(s.generateInfoCasterAttributes[item]),
      true,
      random_
    );

    if (casterIds.length != 0) {
      s._itemCasterAttributes[item.packNftId(itemId)] = casterValues.toBytes32ArrayWithIds(casterIds);
    }

    // TARGET
    (targetIds, targetValues) = _generateSimpleAttributes(
      unpackItemGenerateInfo(s.generateInfoTargetAttributes[item]),
      true,
      random_
    );

    if (targetIds.length != 0) {
      s._itemTargetAttributes[item.packNftId(itemId)] = targetValues.toBytes32ArrayWithIds(targetIds);
    }
  }

  /// @notice Generate all mandatory attributes and try to generate required number of random attributes.
  /// Generate at least {info.minRandomAttributes} of random attributes if it's possible
  /// but not more than {info.maxRandomAttributes}. Value of each attribute is generated randomly according its chances.
  /// @param meta Assume, that meta.min != 0, meta.max != 0 and both meta.min and meta.min should have same sign
  /// because results value cannot be 0
  /// @return ids Ids of the attributes, zero id is allowed
  /// @return values Randomly generated attributes values, min <= value <= max
  /// @return itemRarity Rarity of the item (Either meta.defaultRarity or calculated if there is no default rarity)
  function _generateAttributes(
    IItemController.ItemGenerateInfo memory info,
    IItemController.ItemMeta memory meta,
    function (uint) internal view returns (uint) random_
  ) internal view returns (
    uint8[] memory ids,
    int32[] memory values,
    IItemController.ItemRarity itemRarity
  ) {
    GenerateAttributesContext memory ctx;

    uint len = info.ids.length;
    if (len != 0) {
      ctx.ids = new uint8[](len);
      ctx.values = new int32[](len);
      ctx.usedIndexes = new bool[](len);

      // Fisher–Yates shuffle
      _shuffleInfo(info, random_);

      // initialize ctx by initial values
      // generate all mandatory attributes, try to generate not more than {meta.maxRandomAttributes} random attributes
      _prepareAttributes(info, meta.maxRandomAttributes, ctx, random_);

      // generate missing random attributes if it's necessary, ctx.counter is incremented
      _generateMissingRandomAttributes(info, meta.minRandomAttributes, ctx, random_);

      itemRarity = meta.defaultRarity == 0
        ? _calculateRarity(ctx.randomSum, ctx.chancesSum, ctx.randomAttrCounter, meta.maxRandomAttributes)
        : IItemController.ItemRarity(meta.defaultRarity);
    } else {
      itemRarity = IItemController.ItemRarity.UNKNOWN;
    }

    (ids, values) = _fixLengthsIdsValues(ctx.ids, ctx.values, ctx.counter);
  }

  /// @notice Generate missing random attributes if necessary
  function _generateMissingRandomAttributes(
    IItemController.ItemGenerateInfo memory info,
    uint8 minRandomAttributes,
    GenerateAttributesContext memory ctx,
    function (uint) internal view returns (uint) random_
  ) internal view {
    uint attrToGen = Math.min(ctx.totalRandomAttrsPossible, minRandomAttributes);
    if (ctx.randomAttrCounter < attrToGen && ctx.totalRandomAttrsPossible > ctx.randomAttrCounter) {
      // it's necessary AND possible to generate more random attributes
      uint possibleRemainingAttrs = ctx.totalRandomAttrsPossible - ctx.randomAttrCounter;
      uint remainingAttrsToGen = attrToGen - ctx.randomAttrCounter;

      uint[] memory indicesToGen = new uint[](possibleRemainingAttrs);
      uint indicesToGenCounter;

      // enumerate all attributes, add all indices of not-generated attributes to {indexesToGen}
      for (uint i; i < info.ids.length; ++i) {
        // mandatory attrs should be already generated and no need to check
        if (!ctx.usedIndexes[i]) {
          indicesToGen[indicesToGenCounter] = i;
          indicesToGenCounter++;
        }
      }

      // Shuffle indices of not-generated attributes using Fisher–Yates shuffle
      if (possibleRemainingAttrs > 1) {
        for (uint i; i < possibleRemainingAttrs - 1; ++i) {
          uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, possibleRemainingAttrs - 1, random_);
          (indicesToGen[randomIndex], indicesToGen[i]) = (indicesToGen[i], indicesToGen[randomIndex]);
        }
      }
      // Generate necessary amount of attributes. Fist (shuffled) attributes are selected (MAX_CHANCE is used for each)
      for (uint i; i < remainingAttrsToGen; ++i) {
        uint idx = indicesToGen[i];
        (int32 attr,) = _generateAttribute(info.mins[idx], info.maxs[idx], CalcLib.MAX_CHANCE, random_);
        ctx.ids[ctx.counter] = info.ids[idx];
        ctx.values[ctx.counter] = attr;
        ctx.counter++;
      }
    }
  }

  /// @notice Generate all mandatory attributes, generate not more than {meta.maxRandomAttributes} random attributes.
  /// Updates context:
  ///   {ctx.totalRandomAttrsPossible} - total number of possible random attributes
  ///   {ctx.randomAttrCounter} - total number of generated random attributes  <= {maxRandomAttributes}
  ///   {ctx.randomSum} = sum of random of all random attributes.
  ///   {ctx.chancesSum} = sum of chances of all random attributes.
  ///   {ctx.counter} = total number of generated attributes. Values of ctx.ids, ctx.values, ctx.usedIndexes are
  ///   initialized in the range [0...ctx.counter)
  /// @param ctx Empty struct but arrays ids, values and usedIndexes should be allocated for info.ids.length items
  function _prepareAttributes(
    IItemController.ItemGenerateInfo memory info,
    uint8 maxRandomAttributes,
    GenerateAttributesContext memory ctx,
    function (uint) internal view returns (uint) random_
  ) internal view {
    uint len = info.ids.length;
    for (uint i; i < len; ++i) {
      if (info.chances[i] != CalcLib.MAX_CHANCE) {
        ctx.totalRandomAttrsPossible++;
      }

      if (info.chances[i] >= CalcLib.MAX_CHANCE || !ctx.stopGenerateRandom) {
        (int32 attr, uint random) = _generateAttribute(info.mins[i], info.maxs[i], info.chances[i], random_);

        // count only random attributes for calc rarity
        if (attr != 0) {

          if (
            info.chances[i] < CalcLib.MAX_CHANCE
            // && random != 0 // commented: random = 0 can produce crash in _generateMissingRandomAttributes
          ) {
            ctx.randomAttrCounter++;
            ctx.randomSum += random;
            ctx.chancesSum += info.chances[i];
          }
          ctx.ids[ctx.counter] = info.ids[i];
          ctx.values[ctx.counter] = attr;
          ctx.counter++;
          ctx.usedIndexes[i] = true;
        }

        // it is a bit less fair random for attrs in the end of the list, however we assume it should be pretty rare case
        if (ctx.randomAttrCounter == maxRandomAttributes) {
          ctx.stopGenerateRandom = true;
        }
      }
    }
  }

  /// @notice Shuffle info arrays using Fisher–Yates shuffle algo
  function _shuffleInfo(
    IItemController.ItemGenerateInfo memory info,
    function (uint) internal view returns (uint) random_
  ) internal view {
    uint len = info.ids.length;
    if (len > 1) {
      for (uint i; i < len - 1; i++) {
        uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, len - 1, random_);

        (info.ids[randomIndex], info.ids[i]) = (info.ids[i], info.ids[randomIndex]);
        (info.mins[randomIndex], info.mins[i]) = (info.mins[i], info.mins[randomIndex]);
        (info.maxs[randomIndex], info.maxs[i]) = (info.maxs[i], info.maxs[randomIndex]);
        (info.chances[randomIndex], info.chances[i]) = (info.chances[i], info.chances[randomIndex]);
      }
    }
  }

  /// @notice Generate array [0,1,2.. N-1] and shuffle it using Fisher–Yates shuffle algo
  function _shuffleIndices(
    uint countItems,
    function (uint) internal view returns (uint) random_
  ) internal view returns (uint[] memory indices){
    indices = new uint[](countItems);
    for (uint i = 1; i < countItems; ++i) {
      indices[i] = i;
    }
    if (countItems > 1) {
      for (uint i; i < countItems - 1; i++) {
        uint randomIndex = CalcLib.pseudoRandomInRangeFlex(i, countItems - 1, random_);
        (indices[randomIndex], indices[i]) = (indices[i], indices[randomIndex]);
      }
    }
  }

  /// @notice Reduce lengths of {ids} and {values} to {count}
  function _fixLengthsIdsValues(uint8[] memory ids, int32[] memory values, uint count) internal pure returns (
    uint8[] memory idsOut,
    int32[] memory valuesOut
  ) {
    if (count == ids.length) {
      return (ids, values);
    }

    idsOut = new uint8[](count);
    valuesOut = new int32[](count);
    for (uint i; i < count; ++i) {
      idsOut[i] = ids[i];
      valuesOut[i] = values[i];
    }
    return (idsOut, valuesOut);
  }

  /// @param random_ Pass CalcLib.pseudoRandom here, param is required for unit tests. Max value is MAX_CHANCE
  function _generateSimpleAttributes(
    IItemController.ItemGenerateInfo memory info,
    bool maxChance,
    function (uint) internal view returns (uint) random_
  ) internal view returns (
    uint8[] memory ids,
    int32[] memory values
  ) {
    uint len = info.ids.length;
    ids = new uint8[](len);
    values = new int32[](len);

    uint n = 0;
    for (uint i; i < len; ++i) {
      (int32 attr,) = _generateAttribute(
        info.mins[i],
        info.maxs[i],
        maxChance ? CalcLib.MAX_CHANCE : info.chances[i],
        random_
      );
      if (attr != 0) {
        ids[n] = info.ids[i];
        values[n] = attr;
        ++n;
      }
    }

    return _fixLengthsIdsValues(ids, values, n);
  }

  //endregion ------------------------ Internal logic

  //region ------------------------ Internal utils
  /// @param chance Chance in the range [0...MAX_CHANCE], MAX_CHANCE=1e9 means "mandatory" element.
  /// @param random_ Pass CalcLib.pseudoRandom here, param is required for unit tests
  /// @return attr Either 0 or min <= attr <= max
  /// @return rnd Random value in the range [0...MAX_CHANCE]; It's always 0 for mandatory elements
  function _generateAttribute(
    int32 min,
    int32 max,
    uint32 chance,
    function (uint) internal view returns (uint) random_
  ) internal view returns (
    int32 attr,
    uint rnd
  ) {
    if (chance > CalcLib.MAX_CHANCE) revert IAppErrors.TooHighChance(chance);

    uint diff = uint(CalcLib.absDiff(min, max));

    if (chance < CalcLib.MAX_CHANCE) {
      uint32 random = CalcLib.pseudoRandomUint32Flex(CalcLib.MAX_CHANCE, random_);
      if (random < chance) {
        uint r = uint(CalcLib.MAX_CHANCE - random * (CalcLib.MAX_CHANCE / chance));
        int32 k = int32(int(r * diff / uint(CalcLib.MAX_CHANCE)));
        return (min + k, random);
      }
    } else { // chance == CalcLib.MAX_CHANCE => mandatory element
      if (diff == 0) {
        return (min, 0);
      } else {
        uint r = uint(CalcLib.pseudoRandomUint32Flex(CalcLib.MAX_CHANCE, random_));
        int32 k = int32(int(r % (diff + 1)));

        // return zero random - no need to calc rarity for mandatory elements
        return (min + k, 0);
      }
    }

    return (0, 0);
  }

  /// @notice Calculate item rarity
  /// @param randomSum Total sum random values of all random attributes in ItemGenerateInfo, [0...MAX_CHANCE/attrCounter]
  /// @param chancesSum Total sum of all random chances in ItemGenerateInfo
  /// @param attrCounter Count of random attributes in ItemGenerateInfo
  /// @param maxAttr Index of max allowed random attribute (all attributes with higher indices are not random)
  /// @return item rarity
  function _calculateRarity(uint randomSum, uint chancesSum, uint attrCounter, uint maxAttr) internal pure returns (
    IItemController.ItemRarity
  ) {
    if (attrCounter == 0) {
      return IItemController.ItemRarity.NORMAL;
    }

    uint random = randomSum / attrCounter;
    uint averageChance = chancesSum / attrCounter;

    if (random > CalcLib.MAX_CHANCE) revert IAppErrors.TooHighRandom(random);

    if (random < averageChance / 4 && attrCounter == maxAttr) {
      return IItemController.ItemRarity.RARE;
    } else if (random < averageChance * 3 / 4) {
      return attrCounter > 2
        ? IItemController.ItemRarity.RARE
        : IItemController.ItemRarity.MAGIC;
    } else {
      return attrCounter > 1
        ? IItemController.ItemRarity.MAGIC
        : IItemController.ItemRarity.NORMAL;
    }
  }
  //endregion ------------------------ Internal utils

  //region ------------------------ PACKING

  function packItemGenerateInfo(IItemController.ItemGenerateInfo memory info) internal pure returns (bytes32[] memory result) {
    uint len = info.ids.length;
    if (len != info.mins.length || len != info.maxs.length || len != info.chances.length) {
      revert IAppErrors.LengthsMismatch();
    }

    result = new bytes32[](len);

    for (uint i; i < len; ++i) {
      result[i] = PackingLib.packItemGenerateInfo(info.ids[i], info.mins[i], info.maxs[i], info.chances[i]);
    }
  }

  function unpackItemGenerateInfo(bytes32[] memory gen) internal pure returns (
    IItemController.ItemGenerateInfo memory
  ) {
    uint length = gen.length;

    uint8[] memory ids = new uint8[](length);
    int32[] memory mins = new int32[](length);
    int32[] memory maxs = new int32[](length);
    uint32[] memory chances = new uint32[](length);

    for (uint i; i < length; ++i) {
      (ids[i], mins[i], maxs[i], chances[i]) = gen[i].unpackItemGenerateInfo();
    }

    return IItemController.ItemGenerateInfo(ids, mins, maxs, chances);
  }

  function packItemMeta(IItemController.ItemMeta memory meta) internal pure returns (bytes32) {
    return PackingLib.packItemMeta(
      meta.itemMetaType,
      meta.itemLevel,
      uint8(meta.itemType),
      meta.baseDurability,
      meta.defaultRarity,
      meta.minRandomAttributes,
      meta.maxRandomAttributes,
      meta.manaCost,
      meta.requirements
    );
  }

  function unpackedItemMeta(bytes32 meta) internal pure returns (IItemController.ItemMeta memory result) {
    return meta.unpackItemMeta();
  }

  function packItemInfo(IItemController.ItemInfo memory info) internal pure returns (bytes32) {
    return PackingLib.packItemInfo(uint8(info.rarity), info.augmentationLevel, info.durability);
  }

  function unpackedItemInfo(bytes32 info) internal pure returns (IItemController.ItemInfo memory result) {
    uint8 rarity;
    (rarity, result.augmentationLevel, result.durability) = info.unpackItemInfo();

    result.rarity = IItemController.ItemRarity(rarity);
    return result;
  }

  function packItemAttackInfo(IItemController.AttackInfo memory info) internal pure returns (bytes32) {
    return PackingLib.packItemAttackInfo(
      uint8(info.aType),
      info.min,
      info.max,
      info.attributeFactors.strength,
      info.attributeFactors.dexterity,
      info.attributeFactors.vitality,
      info.attributeFactors.energy
    );
  }

  function unpackItemAttackInfo(bytes32 info) internal pure returns (IItemController.AttackInfo memory result) {
    IStatController.CoreAttributes memory fs;
    uint8 aType;
    (aType, result.min, result.max, fs.strength, fs.dexterity, fs.vitality, fs.energy) = info.unpackItemAttackInfo();

    result.aType = IItemController.AttackType(aType);
    result.attributeFactors = fs;

    return result;
  }
  //endregion ------------------------ PACKING
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IItemController.sol";
import "../interfaces/IStatController.sol";
import "../interfaces/IAppErrors.sol";

library PackingLib {

  //////////////////////////
  // ---- PACKING LOGIC ----
  //////////////////////////

  //region ------------------------------------ COMMON

  function packNftId(address token, uint id) internal pure returns (bytes32 serialized) {
    if (id > uint(type(uint64).max)) revert IAppErrors.TooHighValue(id);
    serialized = bytes32(uint(uint160(token)));
    serialized |= bytes32(uint(uint64(id))) << 160;
  }

  function unpackNftId(bytes32 data) internal pure returns (address token, uint id) {
    token = address(uint160(uint(data)));
    id = uint(data) >> 160;
  }

  function packAddressWithAmount(address token, uint amount) internal pure returns (bytes32 data) {
    if (amount > uint(type(uint96).max)) revert IAppErrors.TooHighValue(amount);
    data = bytes32(uint(uint160(token)));
    data |= bytes32(uint(uint96(amount))) << 160;
  }

  function unpackAddressWithAmount(bytes32 data) internal pure returns (address token, uint amount) {
    token = address(uint160(uint(data)));
    amount = uint(data) >> 160;
  }

  function packItemMintInfo(address item, uint32 chance) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(item)));
    data |= bytes32(uint(chance)) << 160;
  }

  function unpackItemMintInfo(bytes32 data) internal pure returns (address item, uint32 chance) {
    item = address(uint160(uint(data)));
    chance = uint32(uint(data) >> 160);
  }

  /// @param customDataIndex We assume, that two lowest bytes of this string are always zero
  /// So, the string looks like following: 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX0000
  /// Last 2 bytes will be used to encode {value}
  function packCustomDataChange(bytes32 customDataIndex, int16 value) internal pure returns (bytes32 data) {
    if (uint(customDataIndex) != (uint(customDataIndex) >> 16) << 16) revert IAppErrors.IncompatibleInputString();
    data = bytes32(uint(customDataIndex));
    data |= bytes32(uint(uint16(value)));
  }

  function unpackCustomDataChange(bytes32 data) internal pure returns (bytes32 customDataIndex, int16 value) {
    customDataIndex = bytes32((uint(data) >> 16) << 16);
    value = int16(int(uint(uint16(uint(data)))));
  }

  /// @dev min(uint64) + max(uint64) + isHeroData/isMandatory(uint8)
  function packCustomDataRequirements(uint64 min, uint64 max, bool key) internal pure returns (bytes32 data) {
    data = bytes32(uint(min));
    data |= bytes32(uint(max)) << 64;
    data |= bytes32(uint(key ? uint8(1) : uint8(0))) << (64 + 64);
  }

  function unpackCustomDataRequirements(bytes32 data) internal pure returns (uint64 min, uint64 max, bool key) {
    min = uint64(uint(data));
    max = uint64(uint(data) >> 64);
    key = uint8(uint(data) >> (64 + 64)) == uint8(1);
  }

  function packStatsChange(
    uint32 experience,
    int32 heal,
    int32 manaRegen,
    int32 lifeChancesRecovered,
    int32 damage,
    int32 manaConsumed
  ) internal pure returns (bytes32 data) {
    data = bytes32(uint(experience));
    data |= bytes32(uint(uint32(heal))) << 32;
    data |= bytes32(uint(uint32(manaRegen))) << (32 + 32);
    data |= bytes32(uint(uint32(lifeChancesRecovered))) << (32 + 32 + 32);
    data |= bytes32(uint(uint32(damage))) << (32 + 32 + 32 + 32);
    data |= bytes32(uint(uint32(manaConsumed))) << (32 + 32 + 32 + 32 + 32);
  }

  function unpackStatsChange(bytes32 data) internal pure returns (
    uint32 experience,
    int32 heal,
    int32 manaRegen,
    int32 lifeChancesRecovered,
    int32 damage,
    int32 manaConsumed
  ) {
    experience = uint32(uint(data));
    heal = int32(int(uint(data) >> 32));
    manaRegen = int32(int(uint(data) >> (32 + 32)));
    lifeChancesRecovered = int32(int(uint(data) >> (32 + 32 + 32)));
    damage = int32(int(uint(data) >> (32 + 32 + 32 + 32)));
    manaConsumed = int32(int(uint(data) >> (32 + 32 + 32 + 32 + 32)));
  }

  function packNftIdWithValue(address token, uint id, uint32 value) internal pure returns (bytes32 serialized) {
    if (id > uint(type(uint64).max)) revert IAppErrors.TooHighValue(id);
    serialized = bytes32(uint(uint160(token)));
    serialized |= bytes32(uint(uint64(id))) << 160;
    serialized |= bytes32(uint(value)) << 160 + 64;
  }

  function unpackNftIdWithValue(bytes32 data) internal pure returns (address token, uint id, uint32 value) {
    token = address(uint160(uint(data)));
    id = uint64(uint(data) >> 160);
    value = uint32(uint(data) >> 160 + 64);
  }
  //endregion ------------------------------------ COMMON

  //region ------------------------------------ WORLD/BATTLEFIELD MAP

  function packMapObject(address objectAddress, uint64 objectId, uint8 objectType) internal pure returns (bytes32 packedData) {
    packedData = bytes32(bytes20(objectAddress));
    packedData |= bytes32(uint(objectId) << 32);
    packedData |= bytes32(uint(objectType) << 24);
  }

  function unpackMapObject(bytes32 packedData) internal pure returns (address objectAddress, uint64 objectId, uint8 objectType) {
    objectAddress = address(bytes20(packedData));
    objectId = uint64(uint(packedData) >> 32);
    objectType = uint8(uint(packedData) >> 24);
  }

  function packCoordinate(uint128 x, uint128 y) internal pure returns (bytes32 packedData) {
    packedData = bytes32(uint(x));
    packedData |= bytes32(uint(y) << 128);
  }

  function unpackCoordinate(bytes32 packedData) internal pure returns (uint128 x, uint128 y) {
    x = uint128(uint(packedData));
    y = uint128(uint(packedData) >> 128);
  }

  /// @param x Assume x <= max uint64
  /// @param y Assume y <= max uint64
  function packBattlefieldId(uint8 biomeMapFieldId, uint8 territoryNumber, uint128 x, uint128 y) internal pure returns (bytes32 packedData) {
    // 256 => 128 + 128;
    // 1) 128 is used for biomeMapFieldId, territoryNumber and probably other fields in the future
    // 2) 128 is used to store x, y as uint64, uint64

    // we will use uint64 for coordinates assuming it is more than enough for biome map
    packedData = bytes32(uint(biomeMapFieldId));
    packedData |= bytes32(uint(territoryNumber) << (8));
    packedData |= bytes32(uint(uint64(x)) << 128);
    packedData |= bytes32(uint(uint64(y)) << (64 + 128));
  }

  function unpackBattlefieldId(bytes32 packedData) internal pure returns (uint8 biomeMapFieldId, uint8 territoryNumber, uint128 x, uint128 y) {
    biomeMapFieldId = uint8(uint(packedData));
    territoryNumber = uint8(uint(packedData) >> (8));
    x = uint128(uint64(uint(packedData) >> (128)));
    y = uint128(uint64(uint(packedData) >> (64 + 128)));
  }
  //endregion ------------------------------------ WORLD/BATTLEFIELD MAP

  //region ------------------------------------ REINFORCEMENT

  function packReinforcementHeroInfo(uint8 biome, uint128 score, uint8 fee, uint64 stakeTs) internal pure returns (bytes32 packedData) {
    packedData = bytes32(uint(biome));
    packedData |= bytes32(uint(score) << 8);
    packedData |= bytes32(uint(fee) << (8 + 128));
    packedData |= bytes32(uint(stakeTs) << (8 + 128 + 8));
  }

  function unpackReinforcementHeroInfo(bytes32 packedData) internal pure returns (uint8 biome, uint128 score, uint8 fee, uint64 stakeTs) {
    biome = uint8(uint(packedData));
    score = uint128(uint(packedData) >> 8);
    fee = uint8(uint(packedData) >> (8 + 128));
    stakeTs = uint64(uint(packedData) >> (8 + 128 + 8));
  }

  function packConfigReinforcementV2(uint32 min, uint32 max, uint32 lowDivider, uint32 highDivider, uint8 levelLimit) internal pure returns (bytes32 packedData) {
    packedData = bytes32(uint(min));
    packedData |= bytes32(uint(max) << 32);
    packedData |= bytes32(uint(lowDivider) << 64);
    packedData |= bytes32(uint(highDivider) << 96);
    packedData |= bytes32(uint(levelLimit) << 128);
  }

  function unpackConfigReinforcementV2(bytes32 packedData) internal pure returns (uint32 min, uint32 max, uint32 lowDivider, uint32 highDivider, uint8 levelLimit) {
    min = uint32(uint(packedData));
    max = uint32(uint(packedData) >> 32);
    lowDivider = uint32(uint(packedData) >> 64);
    highDivider = uint32(uint(packedData) >> 96);
    levelLimit = uint8(uint(packedData) >> 128);
  }
  //endregion ------------------------------------ REINFORCEMENT

  //region ------------------------------------ DUNGEON

  function packDungeonKey(address heroAdr, uint80 heroId, uint16 dungLogicNum) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(heroAdr)));
    data |= bytes32(uint(heroId)) << 160;
    data |= bytes32(uint(dungLogicNum)) << (160 + 80);
  }

  function unpackDungeonKey(bytes32 data) internal pure returns (address heroAdr, uint80 heroId, uint16 dungLogicNum) {
    heroAdr = address(uint160(uint(data)));
    heroId = uint80(uint(data) >> 160);
    dungLogicNum = uint16(uint(data) >> (160 + 80));
  }

  // --- GAME OBJECTS ---

  function packIterationKey(address heroAdr, uint64 heroId, uint32 objId) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(heroAdr)));
    data |= bytes32(uint(heroId)) << 160;
    data |= bytes32(uint(objId)) << (160 + 64);
  }

  function unpackIterationKey(bytes32 data) internal pure returns (address heroAdr, uint64 heroId, uint32 objId) {
    heroAdr = address(uint160(uint(data)));
    heroId = uint64(uint(data) >> 160);
    objId = uint32(uint(data) >> (160 + 64));
  }

  function packMonsterStats(
    uint8 level,
    uint8 race,
    uint32 experience,
    uint8 maxDropItems
  ) internal pure returns (bytes32 data) {
    data = bytes32(uint(level));
    data |= bytes32(uint(race)) << 8;
    data |= bytes32(uint(experience)) << (8 + 8);
    data |= bytes32(uint(maxDropItems)) << (8 + 8 + 32);
  }

  function unpackMonsterStats(bytes32 data) internal pure returns (
    uint8 level,
    uint8 race,
    uint32 experience,
    uint8 maxDropItems
  ) {
    level = uint8(uint(data));
    race = uint8(uint(data) >> 8);
    experience = uint32(uint(data) >> (8 + 8));
    maxDropItems = uint8(uint(data) >> (8 + 8 + 32));
  }

  function packAttackInfo(
    address attackToken,
    uint64 attackTokenId,
    uint8 attackType
  ) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(attackToken)));
    data |= bytes32(uint(attackTokenId)) << 160;
    data |= bytes32(uint(attackType)) << (160 + 64);
  }

  function unpackAttackInfo(bytes32 data) internal pure returns (
    address attackToken,
    uint64 attackTokenId,
    uint8 attackType
  ) {
    attackToken = address(uint160(uint(data)));
    attackTokenId = uint64(uint(data) >> 160);
    attackType = uint8(uint(data) >> (160 + 64));
  }

  function packPlayedObjKey(address heroAdr, uint64 heroId, uint8 oType, uint8 biome) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(heroAdr)));
    data |= bytes32(uint(heroId)) << 160;
    data |= bytes32(uint(oType)) << (160 + 64);
    data |= bytes32(uint(biome)) << (160 + 64 + 8);
  }

  function unpackPlayedObjKey(bytes32 data) internal pure returns (address heroAdr, uint64 heroId, uint8 oType, uint8 biome) {
    heroAdr = address(uint160(uint(data)));
    heroId = uint64(uint(data) >> 160);
    oType = uint8(uint(data) >> (160 + 64));
    biome = uint8(uint(data) >> (160 + 64 + 8));
  }

  function packGeneratedMonster(bool generated, uint32 amplifier, int32 hp, uint8 turnCounter) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint8(generated ? 1 : 0)));
    data |= bytes32(uint(amplifier)) << 8;
    data |= bytes32(uint(uint32(hp))) << (8 + 32);
    data |= bytes32(uint(turnCounter)) << (8 + 32 + 32);
  }

  function unpackGeneratedMonster(bytes32 data) internal pure returns (bool generated, uint32 amplifier, int32 hp, uint8 turnCounter) {
    generated = uint8(uint(data)) == uint8(1);
    amplifier = uint32(uint(data) >> 8);
    hp = int32(int(uint(data) >> (8 + 32)));
    turnCounter = uint8(uint(data) >> (8 + 32 + 32));
  }
  //endregion ------------------------------------ DUNGEON

  //region ------------------------------------ ITEMS

  /// @notice itemMetaType8 + itemLvl8 + itemType8 + baseDurability16 + defaultRarity8 + minAttr8 + maxAttr8 + manaCost32 + req(packed core 128)
  /// @param itemType This is ItemType enum
  function packItemMeta(
    uint8 itemMetaType,
    uint8 itemLvl,
    uint8 itemType,
    uint16 baseDurability,
    uint8 defaultRarity,
    uint8 minAttr,
    uint8 maxAttr,
    uint32 manaCost,
    IStatController.CoreAttributes memory req
  ) internal pure returns (bytes32 data) {
    data = bytes32(uint(itemMetaType));
    data |= bytes32(uint(itemLvl)) << 8;
    data |= bytes32(uint(itemType)) << (8 + 8);
    data |= bytes32(uint(baseDurability)) << (8 + 8 + 8);
    data |= bytes32(uint(defaultRarity)) << (8 + 8 + 8 + 16);
    data |= bytes32(uint(minAttr)) << (8 + 8 + 8 + 16 + 8);
    data |= bytes32(uint(maxAttr)) << (8 + 8 + 8 + 16 + 8 + 8);
    data |= bytes32(uint(manaCost)) << (8 + 8 + 8 + 16 + 8 + 8 + 8);
    data |= bytes32(uint(int(req.strength))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32);
    data |= bytes32(uint(int(req.dexterity))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32);
    data |= bytes32(uint(int(req.vitality))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32);
    data |= bytes32(uint(int(req.energy))) << (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32 + 32);
  }

  function unpackItemMeta(bytes32 data) internal pure returns (IItemController.ItemMeta memory) {
    IItemController.ItemMeta memory result;

    result.itemMetaType = uint8(uint(data));
    result.itemLevel = uint8(uint(data) >> 8);
    result.itemType = IItemController.ItemType(uint8(uint(data) >> (8 + 8)));
    result.baseDurability = uint16(uint(data) >> (8 + 8 + 8));
    result.defaultRarity = uint8(uint(data) >> (8 + 8 + 8 + 16));
    result.minRandomAttributes = uint8(uint(data) >> (8 + 8 + 8 + 16 + 8));
    result.maxRandomAttributes = uint8(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8));
    result.manaCost = uint32(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8));
    result.requirements.strength = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32)));
    result.requirements.dexterity = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32)));
    result.requirements.vitality = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32)));
    result.requirements.energy = int32(int(uint(data) >> (8 + 8 + 8 + 16 + 8 + 8 + 8 + 32 + 32 + 32 + 32)));

    return result;
  }

  function packItemGenerateInfo(uint8 id, int32 min, int32 max, uint32 chance) internal pure returns (bytes32 data) {
    data = bytes32(uint(id));
    data |= bytes32(uint(uint32(min))) << 8;
    data |= bytes32(uint(uint32(max))) << (8 + 32);
    data |= bytes32(uint(chance)) << (8 + 32 + 32);
  }

  function unpackItemGenerateInfo(bytes32 data) internal pure returns (uint8 id, int32 min, int32 max, uint32 chance) {
    id = uint8(uint(data));
    min = int32(int(uint(data) >> 8));
    max = int32(int(uint(data) >> (8 + 32)));
    chance = uint32(uint(data) >> (8 + 32 + 32));
  }

  function packItemAttackInfo(
    uint8 attackType,
    int32 min,
    int32 max,
    int32 factorStr,
    int32 factorDex,
    int32 factorVit,
    int32 factorEng
  ) internal pure returns (bytes32 data) {
    data = bytes32(uint(attackType));
    data |= bytes32(uint(uint32(min))) << 8;
    data |= bytes32(uint(uint32(max))) << (8 + 32);
    data |= bytes32(uint(int(factorStr))) << (8 + 32 + 32);
    data |= bytes32(uint(int(factorDex))) << (8 + 32 + 32 + 32);
    data |= bytes32(uint(int(factorVit))) << (8 + 32 + 32 + 32 + 32);
    data |= bytes32(uint(int(factorEng))) << (8 + 32 + 32 + 32 + 32 + 32);
  }

  function unpackItemAttackInfo(bytes32 data) internal pure returns (
    uint8 attackType,
    int32 min,
    int32 max,
    int32 factorStr,
    int32 factorDex,
    int32 factorVit,
    int32 factorEng
  ) {
    attackType = uint8(uint(data));
    min = int32(int(uint(data) >> 8));
    max = int32(int(uint(data) >> (8 + 32)));
    factorStr = int32(int(uint(data) >> (8 + 32 + 32)));
    factorDex = int32(int(uint(data) >> (8 + 32 + 32 + 32)));
    factorVit = int32(int(uint(data) >> (8 + 32 + 32 + 32 + 32)));
    factorEng = int32(int(uint(data) >> (8 + 32 + 32 + 32 + 32 + 32)));
  }

  function packItemInfo(uint8 rarity, uint8 augmentationLevel, uint16 durability) internal pure returns (bytes32 data) {
    data = bytes32(uint(rarity));
    data |= bytes32(uint(augmentationLevel)) << 8;
    data |= bytes32(uint(durability)) << (8 + 8);
  }

  function unpackItemInfo(bytes32 data) internal pure returns (uint8 rarity, uint8 augmentationLevel, uint16 durability) {
    rarity = uint8(uint(data));
    augmentationLevel = uint8(uint(data) >> 8);
    durability = uint16(uint(data) >> (8 + 8));
  }
  //endregion ------------------------------------ ITEMS

  //region ------------------------------------ STORIES

  function packStoryPageId(uint16 storyId, uint16 pageId, uint8 heroClass) internal pure returns (bytes32 data) {
    data = bytes32(uint(storyId));
    data |= bytes32(uint(pageId)) << 16;
    data |= bytes32(uint(heroClass)) << (16 + 16);
  }

  function unpackStoryPageId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass) {
    storyId = uint16(uint(data));
    pageId = uint16(uint(data) >> 16);
    heroClass = uint8(uint(data) >> (16 + 16));
  }

  function packStoryAnswerId(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId) internal pure returns (bytes32 data) {
    data = bytes32(uint(storyId));
    data |= bytes32(uint(pageId)) << 16;
    data |= bytes32(uint(heroClass)) << (16 + 16);
    data |= bytes32(uint(answerId)) << (16 + 16 + 8);
  }

  function unpackStoryAnswerId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId) {
    storyId = uint16(uint(data));
    pageId = uint16(uint(data) >> 16);
    heroClass = uint8(uint(data) >> (16 + 16));
    answerId = uint16(uint(data) >> (16 + 16 + 8));
  }

  function packStoryNextPagesId(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 resultId) internal pure returns (bytes32 data) {
    data = bytes32(uint(storyId));
    data |= bytes32(uint(pageId)) << 16;
    data |= bytes32(uint(heroClass)) << (16 + 16);
    data |= bytes32(uint(answerId)) << (16 + 16 + 8);
    data |= bytes32(uint(resultId)) << (16 + 16 + 8 + 16);
  }

  function unpackStoryNextPagesId(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 resultId) {
    storyId = uint16(uint(data));
    pageId = uint16(uint(data) >> 16);
    heroClass = uint8(uint(data) >> (16 + 16));
    answerId = uint16(uint(data) >> (16 + 16 + 8));
    resultId = uint8(uint(data) >> (16 + 16 + 8 + 16));
  }

  function packStoryAttributeRequirement(uint8 attributeIndex, int32 value, bool isCore) internal pure returns (bytes32 data) {
    data = bytes32(uint(attributeIndex));
    data |= bytes32(uint(uint32(value))) << 8;
    data |= bytes32(uint(isCore ? uint8(1) : uint8(0))) << (8 + 32);
  }

  function unpackStoryAttributeRequirement(bytes32 data) internal pure returns (uint8 attributeIndex, int32 value, bool isCore) {
    attributeIndex = uint8(uint(data));
    value = int32(int(uint(data) >> 8));
    isCore = uint8(uint(data) >> (8 + 32)) == uint8(1);
  }

  function packStoryItemRequirement(address item, bool requireItemBurn, bool requireItemEquipped) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(item)));
    data |= bytes32(uint(requireItemBurn ? uint8(1) : uint8(0))) << 160;
    data |= bytes32(uint(requireItemEquipped ? uint8(1) : uint8(0))) << (160 + 8);
  }

  function unpackStoryItemRequirement(bytes32 data) internal pure returns (address item, bool requireItemBurn, bool requireItemEquipped) {
    item = address(uint160(uint(data)));
    requireItemBurn = uint8(uint(data) >> 160) == uint8(1);
    requireItemEquipped = uint8(uint(data) >> (160 + 8)) == uint8(1);
  }

  /// @dev max amount is 309,485,009 for token with 18 decimals
  function packStoryTokenRequirement(address token, uint88 amount, bool requireTransfer) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(token)));
    data |= bytes32(uint(amount)) << 160;
    data |= bytes32(uint(requireTransfer ? uint8(1) : uint8(0))) << (160 + 88);
  }

  function unpackStoryTokenRequirement(bytes32 data) internal pure returns (address token, uint88 amount, bool requireTransfer) {
    token = address(uint160(uint(data)));
    amount = uint88(uint(data) >> 160);
    requireTransfer = uint8(uint(data) >> (160 + 88)) == uint8(1);
  }

  function packStoryCustomDataResult(uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 customDataResultId) internal pure returns (bytes32 data) {
    data = bytes32(uint(storyId));
    data |= bytes32(uint(pageId)) << 16;
    data |= bytes32(uint(heroClass)) << (16 + 16);
    data |= bytes32(uint(answerId)) << (16 + 16 + 8);
    data |= bytes32(uint(customDataResultId)) << (16 + 16 + 8 + 16);
  }

  function unpackStoryCustomDataResult(bytes32 data) internal pure returns (uint16 storyId, uint16 pageId, uint8 heroClass, uint16 answerId, uint8 customDataResultId) {
    storyId = uint16(uint(data));
    pageId = uint16(uint(data) >> 16);
    heroClass = uint8(uint(data) >> (16 + 16));
    answerId = uint16(uint(data) >> (16 + 16 + 8));
    customDataResultId = uint8(uint(data) >> (16 + 16 + 8 + 16));
  }

  function packStoryHeroState(uint16 pageId, uint40 heroLastActionTS) internal pure returns (bytes32 data) {
    data = bytes32(uint(pageId));
    data |= bytes32(uint(heroLastActionTS)) << 16;
  }

  function unpackStoryHeroState(bytes32 data) internal pure returns (uint16 pageId, uint40 heroLastActionTS) {
    pageId = uint16(uint(data));
    heroLastActionTS = uint40(uint(data) >> 16);
  }

  function packStoryHeroStateId(address heroAdr, uint80 heroId, uint16 storyId) internal pure returns (bytes32 data) {
    data = bytes32(uint(uint160(heroAdr)));
    data |= bytes32(uint(heroId)) << 160;
    data |= bytes32(uint(storyId)) << (160 + 80);
  }

  function unpackStoryHeroStateId(bytes32 data) internal pure returns (address heroAdr, uint80 heroId, uint16 storyId) {
    heroAdr = address(uint160(uint(data)));
    heroId = uint80(uint(data) >> 160);
    storyId = uint16(uint(data) >> (160 + 80));
  }

  function packStorySimpleRequirement(uint32 randomRequirement, uint32 delayRequirement, bool isFinalAnswer) internal pure returns (bytes32 data) {
    data = bytes32(uint(randomRequirement));
    data |= bytes32(uint(delayRequirement)) << 32;
    data |= bytes32(uint(isFinalAnswer ? uint8(1) : uint8(0))) << (32 + 32);
  }

  function unpackStorySimpleRequirement(bytes32 data) internal pure returns (uint32 randomRequirement, uint32 delayRequirement, bool isFinalAnswer) {
    randomRequirement = uint32(uint(data));
    delayRequirement = uint32(uint(data) >> 32);
    isFinalAnswer = uint8(uint(data) >> (32 + 32)) == uint8(1);
  }

  function packBreakInfo(uint8 slot, uint64 chance, bool stopIfBroken) internal pure returns (bytes32 data) {
    data = bytes32(uint(slot));
    data |= bytes32(uint(chance)) << 8;
    data |= bytes32(uint(stopIfBroken ? uint8(1) : uint8(0))) << (8 + 64);
  }

  function unpackBreakInfo(bytes32 data) internal pure returns (uint8 slot, uint64 chance, bool stopIfBurned) {
    slot = uint8(uint(data));
    chance = uint64(uint(data) >> 8);
    stopIfBurned = uint8(uint(data) >> (8 + 64)) == uint8(1);
  }
  //endregion ------------------------------------ STORIES

  //region ------------------------------------ Hero controller
  function packTierHero(uint8 tier, address hero) internal pure returns (bytes32 packedTierHero) {
    packedTierHero = bytes32(uint(tier));
    packedTierHero |= bytes32(uint(uint160(hero)) << 8);
  }

  function unpackTierHero(bytes32 packedTierHero) internal pure returns (uint8 tier, address hero) {
    tier = uint8(uint(packedTierHero));
    hero = address(uint160(uint(packedTierHero) >> 8));
  }

  //endregion ------------------------------------ Hero controller

  ////////////////////////////////////////////////////////////////////////////////////
  // ---- ARRAYS LOGIC ----
  ////////////////////////////////////////////////////////////////////////////////////

  //region ------------------------------------ SIMPLE ARRAYS


  function packUint8Array(uint8[] memory data) internal pure returns (bytes32) {
    uint len = data.length;
    if (len > 32) revert IAppErrors.OutOfBounds(len, 32);
    bytes32 result;
    for (uint i = 0; i < len; i++) {
      result |= bytes32(uint(data[i])) << (i * 8);
    }
    return result;
  }

  /// @notice Simple faster version of {packUint8Array} for small number of items
  ///         It allows to exclude dynamic array creation.
  function packUint8Array3(uint8 a, uint8 b, uint8 c) internal pure returns (bytes32) {
    bytes32 result = bytes32(uint(a));
    result |= bytes32(uint(b)) << (1 * 8);
    result |= bytes32(uint(c)) << (2 * 8);
    return result;
  }


  function unpackUint8Array(bytes32 data) internal pure returns (uint8[] memory) {
    uint8[] memory result = new uint8[](32);
    for (uint i = 0; i < 32; i++) {
      result[i] = uint8(uint(data) >> (i * 8));
    }
    return result;
  }

  /// @notice Simple faster version of {unpackUint8Array} for small number of items
  ///         It allows to exclude only first 3 values
  function unpackUint8Array3(bytes32 data) internal pure returns (uint8 a, uint8 b, uint8 c) {
    a = uint8(uint(data));
    b = uint8(uint(data) >> (1 * 8));
    c = uint8(uint(data) >> (2 * 8));
  }

  function changeUnit8ArrayWithCheck(bytes32 data, uint index, uint8 value, uint8 expectedPrevValue) internal pure returns (bytes32 newData) {
    uint8[] memory arr = unpackUint8Array(data);
    if (arr[index] != expectedPrevValue) revert IAppErrors.UnexpectedValue(uint(expectedPrevValue), uint(arr[index]));
    arr[index] = value;
    return packUint8Array(arr);
  }

  function packInt32Array(int32[] memory data) internal pure returns (bytes32) {
    uint len = data.length;
    if (len > 8) revert IAppErrors.OutOfBounds(len, 8);
    bytes32 result;
    for (uint i; i < len; i++) {
      result |= bytes32(uint(uint32(data[i]))) << (i * 32);
    }
    return result;
  }

  function unpackInt32Array(bytes32 data) internal pure returns (int32[] memory) {
    int32[] memory result = new int32[](8);
    for (uint i = 0; i < 8; i++) {
      result[i] = int32(int(uint(data) >> (i * 32)));
    }
    return result;
  }

  function packUint32Array(uint32[] memory data) internal pure returns (bytes32) {
    uint len = data.length;
    if (len > 8) revert IAppErrors.OutOfBounds(len, 8);
    bytes32 result;
    for (uint i = 0; i < len; i++) {
      result |= bytes32(uint(data[i])) << (i * 32);
    }
    return result;
  }

  function unpackUint32Array(bytes32 data) internal pure returns (uint32[] memory) {
    uint32[] memory result = new uint32[](8);
    for (uint i = 0; i < 8; i++) {
      result[i] = uint32(uint(data) >> (i * 32));
    }
    return result;
  }
  //endregion ------------------------------------ SIMPLE ARRAYS

  //region ------------------------------------ COMPLEX ARRAYS

  // We should represent arrays without concrete size.
  // For this reason we must not revert IAppErrors.on out of bounds but return zero value instead.

  // we need it for properly unpack packed arrays with ids
//  function getInt32AsInt24(bytes32[] memory arr, uint idx) internal pure returns (int32) {
//    if (idx / 8 >= arr.length) {
//      return int32(0);
//    }
//    return int32(int24(int(uint(arr[idx / 8]) >> ((idx % 8) * 32))));
//  }

  // we need it for properly unpack packed arrays with ids
//  function getUnit8From32Step(bytes32[] memory arr, uint idx) internal pure returns (uint8) {
//    if (idx / 8 >= arr.length) {
//      return uint8(0);
//    }
//    return uint8(uint(arr[idx / 8]) >> ((idx % 8) * 32 + 24));
//  }

  function getInt32Memory(bytes32[] memory arr, uint idx) internal pure returns (int32) {
    if (idx / 8 >= arr.length) {
      return int32(0);
    }
    return int32(int(uint(arr[idx / 8]) >> ((idx % 8) * 32)));
  }

  function getInt32(bytes32[] storage arr, uint idx) internal view returns (int32) {
    // additional gas usage, but we should not revert IAppErrors.on out of bounds
    if (idx / 8 >= arr.length) {
      return int32(0);
    }
    return int32(int(uint(arr[idx / 8]) >> ((idx % 8) * 32)));
  }

  function setInt32(bytes32[] storage arr, uint idx, int32 value) internal {
    uint pos = idx / 8;
    uint shift = (idx % 8) * 32;

    uint curLength = arr.length;
    if (pos >= curLength) {
      arr.push(0);
      for (uint i = curLength; i < pos; ++i) {
        arr.push(0);
      }
    }

    arr[pos] = bytes32(uint(arr[pos]) & ~(uint(0xffffffff) << shift) | (uint(uint32(value)) & 0xffffffff) << shift);
  }

  /// @notice Increment {idx}-th item on {value}
  function changeInt32(bytes32[] storage arr, uint idx, int32 value) internal returns (int32 newValue, int32 change) {
    int32 cur = int32(int(getInt32(arr, idx)));
    int newValueI = int(cur) + int(value);
    newValue = int32(newValueI);
    change = int32(newValueI - int(cur));

    setInt32(arr, idx, newValue);
  }

  function toInt32Array(bytes32[] memory arr, uint size) internal pure returns (int32[] memory) {
    int32[] memory result = new int32[](size);
    for (uint i = 0; i < arr.length; i++) {
      for (uint j; j < 8; ++j) {
        uint idx = i * 8 + j;
        if (idx >= size) break;
        result[idx] = getInt32Memory(arr, idx);
      }
    }
    return result;
  }

  /// @dev pack int32 array into bytes32 array
  function toBytes32Array(int32[] memory arr) internal pure returns (bytes32[] memory) {
    uint size = arr.length / 8 + 1;
    bytes32[] memory result = new bytes32[](size);
    for (uint i; i < size; ++i) {
      for (uint j; j < 8; ++j) {
        uint idx = i * 8 + j;
        if (idx >= arr.length) break;
        result[i] |= bytes32(uint(uint32(arr[idx]))) << (j * 32);
      }
    }
    return result;
  }

  /// @dev pack int32 array into bytes32 array using last 8bytes for ids
  ///      we can not use zero values coz will not able to properly unpack it later
  function toBytes32ArrayWithIds(int32[] memory arr, uint8[] memory ids) internal pure returns (bytes32[] memory) {
    if (arr.length != ids.length) revert IAppErrors.LengthsMismatch();

    uint size = arr.length / 8 + 1;
    bytes32[] memory result = new bytes32[](size);
    for (uint i; i < size; ++i) {
      for (uint j; j < 8; ++j) {
        uint idx = i * 8 + j;
        if (idx >= arr.length) break;

        if (arr[idx] > type(int24).max || arr[idx] < type(int24).min) revert IAppErrors.IntOutOfRange(int(arr[idx]));
        if (arr[idx] == 0) revert IAppErrors.ZeroValue();
        result[i] |= bytes32(uint(uint24(int24(arr[idx])))) << (j * 32);
        result[i] |= bytes32(uint(ids[idx])) << (j * 32 + 24);
      }
    }
    return result;
  }

  /// @dev we do not know exact size of array, assume zero values is not acceptable for this array
  function toInt32ArrayWithIds(bytes32[] memory arr) internal pure returns (int32[] memory values, uint8[] memory ids) {
    uint len = arr.length;
    uint size = len * 8;
    int32[] memory valuesTmp = new int32[](size);
    uint8[] memory idsTmp = new uint8[](size);
    uint counter;
    for (uint i = 0; i < len; i++) {
      for (uint j; j < 8; ++j) {
        uint idx = i * 8 + j;
        // if (idx >= size) break;  // it looks like a useless check
        valuesTmp[idx] = int32(int24(int(uint(arr[i]) >> (j * 32)))); // getInt32AsInt24(arr, idx);
        idsTmp[idx] = uint8(uint(arr[i]) >> (j * 32 + 24)); // getUnit8From32Step(arr, idx);
        if (valuesTmp[idx] == 0) {
          break;
        }
        counter++;
      }
    }

    values = new int32[](counter);
    ids = new uint8[](counter);
    for (uint i; i < counter; ++i) {
      values[i] = valuesTmp[i];
      ids[i] = idsTmp[i];
    }
  }
  //endregion ------------------------------------ COMPLEX ARRAYS

  //region ------------------------------------ Guilds
  /// @dev ShelterID is uint. But in the code we assume that this ID can be stored as uint64 (see auctions)
  /// @param biome 1, 2, 3...
  /// @param shelterLevel 1, 2 or 3.
  /// @param shelterIndex 0, 1, 2 ...
  function packShelterId(uint8 biome, uint8 shelterLevel, uint8 shelterIndex) internal pure returns (uint) {
    return uint(biome) | (uint(shelterLevel) << 8) | (uint(shelterIndex) << 16);
  }

  function unpackShelterId(uint shelterId) internal pure returns (uint8 biome, uint8 shelterLevel, uint8 shelterIndex) {
    return (uint8(shelterId), uint8(shelterId >> 8), uint8(shelterId >> 16));
  }
  //endregion ------------------------------------ Guilds

  //region ------------------------------------ Metadata of IItemController.OtherSubtypeKind

  function getOtherItemTypeKind(bytes memory packedData) internal pure returns (IItemController.OtherSubtypeKind) {
    bytes32 serialized;
    assembly {
      serialized := mload(add(packedData, 32))
    }
    uint8 kind = uint8(uint(serialized));
    if (kind == 0 || kind >= uint8(IItemController.OtherSubtypeKind.END_SLOT)) revert IAppErrors.IncorrectOtherItemTypeKind(kind);
    return IItemController.OtherSubtypeKind(kind);
  }

  function packOtherItemReduceFragility(uint value) internal pure returns (bytes memory packedData) {
    bytes32 serialized = bytes32(uint(uint8(IItemController.OtherSubtypeKind.REDUCE_FRAGILITY_1)));
    serialized |= bytes32(uint(uint248(value))) << 8;
    return bytes.concat(serialized);
  }

  function unpackOtherItemReduceFragility(bytes memory packedData) internal pure returns (uint) {
    bytes32 serialized;
    assembly {
      serialized := mload(add(packedData, 32))
    }
    uint8 kind = uint8(uint(serialized));
    if (kind != uint8(IItemController.OtherSubtypeKind.REDUCE_FRAGILITY_1)) revert IAppErrors.IncorrectOtherItemTypeKind(kind);
    uint value = uint248(uint(serialized) >> 8);
    return value;
  }
  //endregion ------------------------------------ Metadata of IItemController.OtherSubtypeKind
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IAppErrors.sol";
import "../interfaces/IApplicationEvents.sol";
import "../interfaces/IShelterController.sol";
import "../interfaces/IShelterController.sol";
import "../interfaces/IUserController.sol";
import "../lib/StringLib.sol";
import "../token/GuildBank.sol";
import "./StatLib.sol";
import "../interfaces/IShelterAuction.sol";

library ShelterLib {
  using EnumerableSet for EnumerableSet.UintSet;
  using EnumerableSet for EnumerableSet.AddressSet;
  using EnumerableSet for EnumerableSet.UintSet;

  //region ------------------------ Constants
  /// @dev keccak256(abi.encode(uint256(keccak256("shelter.controller.main")) - 1)) & ~bytes32(uint256(0xff))
  bytes32 internal constant SHELTER_CONTROLLER_STORAGE_LOCATION = 0x5a293071b39954a4fcf98ae7184af7c6201e972e15842b884f1ad071e9bded00; // shelter.controller.main

  uint8 internal constant MIN_SHELTER_LEVEL = 1;
  uint8 internal constant MAX_SHELTER_LEVEL = 3;
  //endregion ------------------------ Constants

  //region ------------------------ Restrictions
  function _onlyDeployer(IController controller) internal view {
    if (!controller.isDeployer(msg.sender)) revert IAppErrors.ErrorNotDeployer(msg.sender);
  }

  function _onlyGuildController(address guildController) internal view {
    if (msg.sender != guildController) revert IAppErrors.ErrorNotGuildController();
  }

  function _notPaused(IController controller) internal view {
    if (controller.onPause()) revert IAppErrors.ErrorPaused();
  }
  //endregion ------------------------ Restrictions

  //region ------------------------ Storage

  function _S() internal pure returns (IShelterController.MainState storage s) {
    assembly {
      s.slot := SHELTER_CONTROLLER_STORAGE_LOCATION
    }
    return s;
  }
  //endregion ------------------------ Storage

  //region ------------------------ Shelter view
  /// @notice Get list of all registered shelters in the given {biome}
  function getShelters(uint8 biome) internal view returns (uint[] memory shelterIds) {
    return _S().shelters[biome].values();
  }

  /// @notice Get initial price of the given shelter. The price is used if the shelter doesn't belong to any guild
  function getShelterPrice(uint shelterId) internal view returns (uint price) {
    return _S().shelterPrices[shelterId];
  }

  /// @notice Get shelter which belongs to the given guild
  function guildToShelter(uint guildId) internal view returns (uint shelterId) {
    return _S().guildToShelter[guildId];
  }

  /// @notice Get guild to which the given shelter belongs
  function shelterToGuild(uint shelterId) internal view returns (uint guildId) {
    return _S().shelterToGuild[shelterId];
  }

  /// @notice Get shelter of the guild to which the user belongs
  function getShelterId(IGuildController guildController, address user) internal view returns (uint shelterId) {
    uint guildId = guildController.memberOf(user);
    return guildId == 0
      ? 0
      : _S().guildToShelter[guildId];
  }

  /// @notice List of items that can be bought in the shelter of the given level in the given biome
  function getShelterItems(uint shelterId) internal view returns (address[] memory items) {
    return _S().shelterItems[shelterId].values();
  }

  function getShelterItemData(uint shelterId, address item) internal view returns (
    uint64 priceInPvpPoints,
    uint128 priceInGameToken,
    uint16 maxItemsPerDayLimit
  ) {
    IShelterController.ShelterItemData memory data = _S().shelterItemData[shelterId][item];
    return (
      data.priceInPvpPoints,
      data.priceInGameToken,
      data.maxItemsPerDayLimit
    );
  }

  /// @notice How many {item} instances were purchased per {epochDay} in the given {shelterId}
  /// @param epochDay TimestampInSeconds / 24 * 60 * 60
  function getCountPurchasedItems(address item, uint shelterId, uint32 epochDay) internal view returns (uint) {
    return _S().countPurchasedItems[shelterId][epochDay][item];
  }

  //endregion ------------------------ Shelter view

  //region ------------------------ Shelter config
  /// @notice Register new shelter or overwrite exist. Only registered shelters can be purchased.
  /// @param shelterId ID should be generated using {PackingLib.packShelterId}
  /// @param price Initial shelter price in game tokens
  function setShelter(IController controller, uint shelterId, uint price) internal {
    ShelterLib._onlyDeployer(controller);

    (uint8 biome, uint8 shelterLevel, ) = PackingLib.unpackShelterId(shelterId);

    if (biome == 0 || biome > StatLib.MAX_POSSIBLE_BIOME) revert IAppErrors.ErrorIncorrectBiome(biome);
    if (price == 0) revert IAppErrors.ZeroValueNotAllowed();
    if (shelterLevel < MIN_SHELTER_LEVEL || shelterLevel > MAX_SHELTER_LEVEL) revert IAppErrors.IncorrectShelterLevel(shelterLevel);

    _S().shelterPrices[shelterId] = price;
    _S().shelters[biome].add(shelterId);

    emit IApplicationEvents.RegisterShelter(shelterId, price);
  }

  /// @notice Set items that can be purchases in the given shelter: remove previously stored items, add new items.
  /// @param shelterId ID should be generated using {PackingLib.packShelterId}
  /// @param items List of item tokens
  /// @param pricesInPvpPoints Prices in pvp-points. The points are taken from guild balance at the moment of purchasing
  /// @param pricesInGameTokens Additional prices in game tokens. Can contain zeros.
  /// @param maxItemsPerDayLimits Indicate how many item instances the users can purchase per day. 0 - no limitations
  function setShelterItems(
    IController controller,
    uint shelterId,
    address[] memory items,
    uint64[] memory pricesInPvpPoints,
    uint128[] memory pricesInGameTokens,
    uint16[] memory maxItemsPerDayLimits
  ) internal {
    ShelterLib._onlyDeployer(controller);

    uint len = items.length;
    if (len != pricesInPvpPoints.length || len != pricesInGameTokens.length || len != maxItemsPerDayLimits.length) {
      revert IAppErrors.LengthsMismatch();
    }

    EnumerableSet.AddressSet storage set = _S().shelterItems[shelterId];

    // remove previously stored items
    address[] memory prevItems = set.values();
    uint prevItemsLen = prevItems.length;
    for (uint i; i < prevItemsLen; ++i) {
      set.remove(prevItems[i]);
      delete _S().shelterItemData[shelterId][prevItems[i]];
    }

    // add new items
    for (uint i; i < len; ++i) {
      set.add(items[i]);
      if (pricesInPvpPoints[i] == 0 && pricesInGameTokens[i] == 0) revert IAppErrors.FreeShelterItemsAreNotAllowed(shelterId, items[i]);
      _S().shelterItemData[shelterId][items[i]] = IShelterController.ShelterItemData({
        priceInPvpPoints: pricesInPvpPoints[i],
        priceInGameToken: pricesInGameTokens[i],
        maxItemsPerDayLimit: maxItemsPerDayLimits[i]
      });
    }

    emit IApplicationEvents.SetShelterItems(shelterId, items, pricesInPvpPoints, pricesInGameTokens, maxItemsPerDayLimits);
  }
  //endregion ------------------------ Shelter config

  //region ------------------------ Shelter actions

  /// @notice Guild buys a shelter that doesn't belong to any guild. It pays default prices and changes owner of the shelter.
  function buyShelter(IController controller, address msgSender, uint shelterId) internal {
    _notPaused(controller);

    IGuildController guildController = IGuildController(controller.guildController());
    (uint guildId,) = guildController.checkPermissions(msgSender, uint(IGuildController.GuildRightBits.CHANGE_SHELTER_3));

    // only registered shelter can be purchased
    (uint8 biome, , ) = PackingLib.unpackShelterId(shelterId);
    if (!_S().shelters[biome].contains(shelterId)) revert IAppErrors.ShelterIsNotRegistered();

    // Each guild is able to have only 1 shelter. Exist shelter should be sold or left
    if (_S().guildToShelter[guildId] != 0) revert IAppErrors.GuildAlreadyHasShelter();
    if (_S().shelterToGuild[shelterId] != 0) revert IAppErrors.ShelterIsBusy();

    { // Shelter can be bought only if there is no auction bid
      address shelterAuction = guildController.shelterAuctionController();
      if (shelterAuction != address(0)) {
        (uint positionId,) = IShelterAuction(shelterAuction).positionByBuyer(guildId);
        if (positionId != 0) revert IAppErrors.AuctionBidOpened(positionId);
      }
    }

    // pay for the shelter from the guild bank
    uint shelterPrice = getShelterPrice(shelterId);
    guildController.payFromGuildBank(guildId, shelterPrice);

    // register ownership
    _S().guildToShelter[guildId] = shelterId;
    _S().shelterToGuild[shelterId] = guildId;

    emit IApplicationEvents.BuyShelter(guildId, shelterId);
  }

  /// @notice Guild leaves the shelter. The shelter becomes free, it can be bought by any guild by default price
  function leaveShelter(IController controller, address msgSender, uint shelterId) internal {
    _notPaused(controller);

    IGuildController guildController = IGuildController(controller.guildController());
    (uint guildId,) = guildController.checkPermissions(msgSender, uint(IGuildController.GuildRightBits.CHANGE_SHELTER_3));

    if (_S().guildToShelter[guildId] != shelterId) revert IAppErrors.ShelterIsNotOwnedByTheGuild();
    if (shelterId == 0) revert IAppErrors.GuildHasNoShelter();

    { // Shelter can be sold only if there is no opened auction position
      address shelterAuction = guildController.shelterAuctionController();
      if (shelterAuction != address(0)) {
        uint positionId = IShelterAuction(shelterAuction).positionBySeller(guildId);
        if (positionId != 0) revert IAppErrors.AuctionPositionOpened(positionId);
      }
    }

    // unregister ownership
    delete _S().guildToShelter[guildId];
    delete _S().shelterToGuild[shelterId];

    emit IApplicationEvents.LeaveShelter(guildId, shelterId);
  }

  /// @notice Purchase the {item} in the shelter that belongs to the guild to which {msgSender} belongs
  function purchaseShelterItem(IController controller, address msgSender, address item, uint blockTimestamp) internal {
    _notPaused(controller);

    IGuildController guildController = IGuildController(controller.guildController());
    // no permission are required - any guild member is able to purchase shelter item
    // but the member should either be owner or should have enough pvp-points capacity, see restriction below
    uint guildId = _getValidGuildId(guildController, msgSender);

    uint shelterId = _S().guildToShelter[guildId];
    if (shelterId == 0) revert IAppErrors.GuildHasNoShelter();

    if (! _S().shelterItems[shelterId].contains(item)) revert IAppErrors.ShelterHasNotItem(shelterId, item);

    // total number of the item instances that can be minted per day CAN BE limited
    IShelterController.ShelterItemData memory itemData = _S().shelterItemData[shelterId][item];
    uint numSoldItems;
    {
      uint32 epochDay = uint32(blockTimestamp / 86400);

      mapping(address => uint) storage countPurchasedItems = _S().countPurchasedItems[shelterId][epochDay];
      numSoldItems = countPurchasedItems[item];

      if (itemData.maxItemsPerDayLimit != 0) {
        if (numSoldItems >= itemData.maxItemsPerDayLimit) revert IAppErrors.MaxNumberItemsSoldToday(numSoldItems, itemData.maxItemsPerDayLimit);
      }
      countPurchasedItems[item] = numSoldItems + 1;
    }

    // user pays for the item by pvp-points and/or by game token (it depends on the item settings)
    if (itemData.priceInPvpPoints != 0) {
      guildController.usePvpPoints(guildId, msgSender, itemData.priceInPvpPoints);
    }

    if (itemData.priceInGameToken != 0) {
      guildController.payFromBalance(itemData.priceInGameToken, msgSender);
      //_process(controller, itemData.priceInGameToken, msgSender);
    }

    // mint the item
    IItemController(controller.itemController()).mint(item, msgSender);

    emit IApplicationEvents.PurchaseShelterItem(msgSender, item, numSoldItems + 1, itemData.priceInPvpPoints, itemData.priceInGameToken);
  }

  /// @notice clear necessary data to indicate that the guiles leaves the shelter
  function clearShelter(address guildController, uint guildId) internal {
    _onlyGuildController(guildController);

    uint shelterId = _S().guildToShelter[guildId];
    if (shelterId != 0) {
      // assume, that msgSender shouldn't have permission CHANGE_SHELTER_3 here

      // ensure that there is no open position for the shelter on auction
      address shelterAuction = IGuildController(guildController).shelterAuctionController();
      if (shelterAuction != address(0)) {
        uint positionId = IShelterAuction(shelterAuction).positionBySeller(guildId);
        if (positionId != 0) revert IAppErrors.AuctionPositionOpened(positionId);
      }

      delete _S().guildToShelter[guildId];
      delete _S().shelterToGuild[shelterId];

      emit IApplicationEvents.LeaveShelter(guildId, shelterId);
    }

  }
  //endregion ------------------------ Shelter actions

  //region ------------------------ Interaction with auctions
  function changeShelterOwner(IController controller, uint shelterId, uint newOwnerGuildId) internal {
    // we assume, that all checks are performed on ShelterAuction side, so we need min checks here
    address shelterAuction = IGuildController(controller.guildController()).shelterAuctionController();
    if (shelterAuction == address(0) || msg.sender != shelterAuction) revert IAppErrors.NotShelterAuction();

    uint prevGuildId = _S().shelterToGuild[shelterId];
    delete _S().guildToShelter[prevGuildId];
    _S().shelterToGuild[shelterId] = newOwnerGuildId;
    _S().guildToShelter[newOwnerGuildId] = shelterId;

    emit IApplicationEvents.ChangeShelterOwner(shelterId, prevGuildId, newOwnerGuildId);
  }

  //endregion ------------------------ Interaction with auctions

  //region ------------------------ Internal logic
  function _getValidGuildId(IGuildController guildController, address user) internal view returns (uint guildId) {
    guildId = guildController.memberOf(user);
    if (guildId == 0) revert IAppErrors.NotGuildMember();
  }
  //endregion ------------------------ Internal logic


}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IStatController.sol";
import "../interfaces/IHeroController.sol";
import "../interfaces/IAppErrors.sol";
import "../openzeppelin/Math.sol";
import "./CalcLib.sol";
import "./PackingLib.sol";

library StatLib {
  using PackingLib for bytes32[];
  using PackingLib for bytes32;
  using PackingLib for uint32[];
  using PackingLib for int32[];
  using CalcLib for int32;

  //region --------------------------- Constants

  /// @notice Version of the contract
  /// @dev Should be incremented when contract changed
  string public constant STAT_LIB_VERSION = "1.0.0";
  uint32 public constant MAX_LEVEL = 99;

  uint public constant BASE_EXPERIENCE = 100_000;
  uint public constant BIOME_LEVEL_STEP = 5;
  uint internal constant _MAX_AMPLIFIER = 1e18;
  uint private constant _PRECISION = 1e18;
  uint private constant VIRTUAL_LEVEL_GAP = 2;

  /// @dev Assume MAX_BIOME * BIOME_LEVEL_STEP < MAX_LEVEL + 1, see dungeonTreasuryReward
  uint public constant MAX_POSSIBLE_BIOME = 19;
  //endregion --------------------------- Constants

  //region --------------------------- Data types

  struct BaseMultiplier {
    uint minDamage;
    uint maxDamage;
    uint attackRating;
    uint defense;
    uint blockRating;
    uint life;
    uint mana;
  }

  struct LevelUp {
    uint life;
    uint mana;
  }

  struct InitialHero {
    IStatController.CoreAttributes core;
    BaseMultiplier multiplier;
    LevelUp levelUp;
    int32 baseLifeChances;
  }

  enum HeroClasses {
    UNKNOWN,
    THRALL,
    SAVAGE,
    MAGE,
    ASSASSIN,
    GHOST,
    HAMMERGINA,
    END_SLOT
  }
  //endregion --------------------------- Data types

  //region --------------------------- BASE

  function isNetworkWithOldSavage() public view returns (bool) {
    return block.chainid == uint(111188) || block.chainid == uint(250);
  }

  // --- HERO 1 (Slave) ---

  function initialHero1() internal pure returns (InitialHero memory) {
    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 15,
      dexterity: 15,
      vitality: 30,
      energy: 10
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.1e18,
      maxDamage: 0.2e18,
      attackRating: 2e18,
      defense: 2e18,
      blockRating: 0.1e18,
      life: 1.5e18,
      mana: 0.5e18
    }),

      levelUp: LevelUp({
      life: 2e18,
      mana: 1e18
    }),

      baseLifeChances: 5
    });
  }

  // --- HERO 2 (Spata) ---

  function initialHero2() internal view returns (InitialHero memory) {

    bool old = isNetworkWithOldSavage();

    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 30,
      dexterity: 5,
      vitality: 25,
      energy: 10
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.15e18,
      maxDamage: old ? 0.25e18 : 0.5e18,
      attackRating: old ? 2e18 : 3e18,
      defense: 1e18,
      blockRating: 0.08e18,
      life: 1.3e18,
      mana: 0.5e18
    }),

      levelUp: LevelUp({
      life: 1.8e18,
      mana: 1e18
    }),

      baseLifeChances: 5
    });
  }

  // --- HERO 3 (Decidia) ---

  function initialHero3() internal pure returns (InitialHero memory) {
    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 10,
      dexterity: 15,
      vitality: 20,
      energy: 25
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.1e18,
      maxDamage: 0.2e18,
      attackRating: 2e18,
      defense: 1e18,
      blockRating: 0.1e18,
      life: 1e18,
      mana: 2e18
    }),

      levelUp: LevelUp({
      life: 1.3e18,
      mana: 2e18
    }),

      baseLifeChances: 5
    });
  }

  // --- HERO 4 (Innatus) ---

  function initialHero4() internal pure returns (InitialHero memory) {
    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 15,
      dexterity: 25,
      vitality: 15,
      energy: 15
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.1e18,
      maxDamage: 0.2e18,
      attackRating: 4e18,
      defense: 3e18,
      blockRating: 0.2e18,
      life: 1.2e18,
      mana: 1e18
    }),

      levelUp: LevelUp({
      life: 1.7e18,
      mana: 1.5e18
    }),

      baseLifeChances: 5
    });
  }

  // --- HERO 5 (F2P) ---

  function initialHero5() internal pure returns (InitialHero memory) {
    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 20,
      dexterity: 20,
      vitality: 20,
      energy: 10
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.15e18,
      maxDamage: 0.25e18,
      attackRating: 3e18,
      defense: 2.5e18,
      blockRating: 0.15e18,
      life: 1.5e18,
      mana: 1.5e18
    }),

      levelUp: LevelUp({
      life: 1.5e18,
      mana: 1.5e18
    }),

      baseLifeChances: 1
    });
  }

  // --- HERO 6 (F2P) HAMMERGINA ---

  function initialHero6() internal pure returns (InitialHero memory) {
    return InitialHero({
      core: IStatController.CoreAttributes({
      strength: 50,
      dexterity: 30,
      vitality: 50,
      energy: 15
    }),

      multiplier: BaseMultiplier({
      minDamage: 0.2e18,
      maxDamage: 0.3e18,
      attackRating: 5e18,
      defense: 3e18,
      blockRating: 0.15e18,
      life: 2e18,
      mana: 2e18
    }),

      levelUp: LevelUp({
      life: 1.7e18,
      mana: 1.5e18
    }),

      baseLifeChances: 1
    });
  }

  // ------

  function initialHero(uint heroClass) internal view returns (InitialHero memory) {
    if (heroClass == 1) {
      return initialHero1();
    } else if (heroClass == 2) {
      return initialHero2();
    } else if (heroClass == 3) {
      return initialHero3();
    } else if (heroClass == 4) {
      return initialHero4();
    } else if (heroClass == 5) {
      return initialHero5();
    } else if (heroClass == 6) {
      return initialHero6();
    } else {
      revert IAppErrors.UnknownHeroClass(heroClass);
    }
  }
  //endregion --------------------------- BASE

  //region --------------------------- CALCULATIONS

  function minDamage(int32 strength, uint heroClass) internal view returns (int32) {
    return int32(int(strength.toUint() * initialHero(heroClass).multiplier.minDamage / _PRECISION));
  }

  function maxDamage(int32 strength, uint heroClass) internal view returns (int32){
    return int32(int(strength.toUint() * initialHero(heroClass).multiplier.maxDamage / _PRECISION));
  }

  function attackRating(int32 dexterity, uint heroClass) internal view returns (int32){
    return int32(int(dexterity.toUint() * initialHero(heroClass).multiplier.attackRating / _PRECISION));
  }

  function defense(int32 dexterity, uint heroClass) internal view returns (int32){
    return int32(int(dexterity.toUint() * initialHero(heroClass).multiplier.defense / _PRECISION));
  }

  function blockRating(int32 dexterity, uint heroClass) internal view returns (int32){
    return int32(int(Math.min((dexterity.toUint() * initialHero(heroClass).multiplier.blockRating / _PRECISION), 75)));
  }

  function life(int32 vitality, uint heroClass, uint32 level) internal view returns (int32){
    return int32(int(
      (vitality.toUint() * initialHero(heroClass).multiplier.life / _PRECISION)
      + (uint(level) * initialHero(heroClass).levelUp.life / _PRECISION)
    ));
  }

  function mana(int32 energy, uint heroClass, uint32 level) internal view returns (int32){
    return int32(int(
      (energy.toUint() * initialHero(heroClass).multiplier.mana / _PRECISION)
      + (uint(level) * initialHero(heroClass).levelUp.mana / _PRECISION)
    ));
  }

  function lifeChances(uint heroClass, uint32 /*level*/) internal view returns (int32){
    return initialHero(heroClass).baseLifeChances;
  }

  function levelExperience(uint32 level) internal pure returns (uint32) {
    if (level == 0 || level >= MAX_LEVEL) {
      return 0;
    }
    return uint32(uint(level) * BASE_EXPERIENCE * (67e17 - CalcLib.log2((uint(MAX_LEVEL - level + 2)) * 1e18)) / 1e18);
  }

  function chanceToHit(
    uint attackersAttackRating,
    uint defendersDefenceRating,
    uint attackersLevel,
    uint defendersLevel,
    uint arFactor
  ) internal pure returns (uint) {
    attackersAttackRating += attackersAttackRating * arFactor / 100;
    uint x = Math.max(attackersAttackRating, 1);
    uint y = Math.max(attackersAttackRating + defendersDefenceRating, 1);
    uint z = attackersLevel;
    uint k = defendersLevel / 2;
    uint xy = x * 1e18 / y;
    uint zk = z * 1e18 / (attackersLevel + k);
    uint base = 2 * xy * zk / 1e18;
    return Math.max(Math.min(base, 0.95e18), 0.2e18);
  }

  function experienceToVirtualLevel(uint experience, uint startFromLevel) internal pure returns (uint level) {
    level = startFromLevel;
    for (; level < MAX_LEVEL;) {
      if (levelExperience(uint32(level)) >= (experience + 1)) {
        break;
      }
      unchecked{++level;}
    }
  }

  function expPerMonster(uint32 monsterExp, uint monsterRarity, uint32 /*heroExp*/, uint32 /*heroCurrentLvl*/, uint /*monsterBiome*/) internal pure returns (uint32) {
    // do not reduce exp per level, it is no economical sense
    return uint32(uint(monsterExp) + uint(monsterExp) * monsterRarity / _MAX_AMPLIFIER);
  }

  /// @notice Allow to calculate delta param for {mintDropChance}
  function mintDropChanceDelta(uint heroCurrentExp, uint heroCurrentLevel, uint monsterBiome) internal pure returns (uint) {
    uint heroBiome = getVirtualLevel(heroCurrentExp, heroCurrentLevel, true) / StatLib.BIOME_LEVEL_STEP + 1;
    return heroBiome > monsterBiome ? 2 ** (heroBiome - monsterBiome + 10) : 0;
  }

  function getVirtualLevel(uint heroCurrentExp, uint heroCurrentLevel, bool withGap) internal pure returns (uint) {
    uint virtualLevel = StatLib.experienceToVirtualLevel(heroCurrentExp, heroCurrentLevel);
    if (withGap && (virtualLevel + 1) > VIRTUAL_LEVEL_GAP) {
      virtualLevel -= VIRTUAL_LEVEL_GAP;
    }
    return virtualLevel;
  }

  function initAttributes(
    bytes32[] storage attributes,
    uint heroClass,
    uint32 level,
    IStatController.CoreAttributes memory base
  ) internal returns (uint32[] memory result) {

    attributes.setInt32(uint(IStatController.ATTRIBUTES.STRENGTH), base.strength);
    attributes.setInt32(uint(IStatController.ATTRIBUTES.DEXTERITY), base.dexterity);
    attributes.setInt32(uint(IStatController.ATTRIBUTES.VITALITY), base.vitality);
    attributes.setInt32(uint(IStatController.ATTRIBUTES.ENERGY), base.energy);

    attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN), minDamage(base.strength, heroClass));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX), maxDamage(base.strength, heroClass));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING), attackRating(base.dexterity, heroClass));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.DEFENSE), defense(base.dexterity, heroClass));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING), blockRating(base.dexterity, heroClass));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE), life(base.vitality, heroClass, level));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.MANA), mana(base.energy, heroClass, level));
    attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE_CHANCES), lifeChances(heroClass, level));

    result = new uint32[](3);
    result[0] = uint32(life(base.vitality, heroClass, level).toUint());
    result[1] = uint32(mana(base.energy, heroClass, level).toUint());
    result[2] = uint32(lifeChances(heroClass, uint32(level)).toUint());
  }

  function updateCoreDependAttributesInMemory(
    int32[] memory attributes,
    int32[] memory bonus,
    uint heroClass,
    uint32 level
  ) internal view returns (int32[] memory) {
    int32 strength = attributes[uint(IStatController.ATTRIBUTES.STRENGTH)];
    int32 dexterity = attributes[uint(IStatController.ATTRIBUTES.DEXTERITY)];
    int32 vitality = attributes[uint(IStatController.ATTRIBUTES.VITALITY)];
    int32 energy = attributes[uint(IStatController.ATTRIBUTES.ENERGY)];

    attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)] = minDamage(strength, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DAMAGE_MIN)];
    attributes[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)] = maxDamage(strength, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DAMAGE_MAX)];
    attributes[uint(IStatController.ATTRIBUTES.ATTACK_RATING)] = attackRating(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.ATTACK_RATING)];
    attributes[uint(IStatController.ATTRIBUTES.DEFENSE)] = defense(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.DEFENSE)];
    attributes[uint(IStatController.ATTRIBUTES.BLOCK_RATING)] = blockRating(dexterity, heroClass) + bonus[uint(IStatController.ATTRIBUTES.BLOCK_RATING)];
    attributes[uint(IStatController.ATTRIBUTES.LIFE)] = life(vitality, heroClass, level) + bonus[uint(IStatController.ATTRIBUTES.LIFE)];
    attributes[uint(IStatController.ATTRIBUTES.MANA)] = mana(energy, heroClass, level) + bonus[uint(IStatController.ATTRIBUTES.MANA)];
    return attributes;
  }

  function updateCoreDependAttributes(
    IController controller,
    bytes32[] storage attributes,
    bytes32[] storage bonusMain,
    bytes32[] storage bonusExtra,
    IStatController.ChangeableStats memory _heroStats,
    uint index,
    address heroToken,
    int32 base
  ) internal {
    uint heroClass = IHeroController(controller.heroController()).heroClass(heroToken);
    if (index == uint(IStatController.ATTRIBUTES.STRENGTH)) {

      attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN),
        StatLib.minDamage(base, heroClass)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MIN))
      );
      attributes.setInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX),
        StatLib.maxDamage(base, heroClass)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DAMAGE_MAX))
      );
    } else if (index == uint(IStatController.ATTRIBUTES.DEXTERITY)) {

      attributes.setInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING),
        StatLib.attackRating(base, heroClass)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.ATTACK_RATING))
      );

      attributes.setInt32(uint(IStatController.ATTRIBUTES.DEFENSE),
        StatLib.defense(base, heroClass)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.DEFENSE))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.DEFENSE))
      );

      attributes.setInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING),
        StatLib.blockRating(base, heroClass)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.BLOCK_RATING))
      );
    } else if (index == uint(IStatController.ATTRIBUTES.VITALITY)) {

      attributes.setInt32(uint(IStatController.ATTRIBUTES.LIFE),
        StatLib.life(base, heroClass, _heroStats.level)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.LIFE))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.LIFE))
      );
    } else if (index == uint(IStatController.ATTRIBUTES.ENERGY)) {

      attributes.setInt32(uint(IStatController.ATTRIBUTES.MANA),
        StatLib.mana(base, heroClass, _heroStats.level)
        + bonusMain.getInt32(uint(IStatController.ATTRIBUTES.MANA))
        + bonusExtra.getInt32(uint(IStatController.ATTRIBUTES.MANA))
      );
    }
  }

  function attributesAdd(int32[] memory base, int32[] memory add) internal pure returns (int32[] memory) {
    unchecked{
      for (uint i; i < base.length; ++i) {
        base[i] += add[i];
      }
    }
    return base;
  }

// Currently this function is not used
//  function attributesRemove(int32[] memory base, int32[] memory remove) internal pure returns (int32[] memory) {
//    unchecked{
//      for (uint i; i < base.length; ++i) {
//        base[i] = CalcLib.minusWithMinFloorI32(base[i], remove[i]);
//      }
//    }
//    return base;
//  }

  function packChangeableStats(IStatController.ChangeableStats memory stats) internal pure returns (bytes32) {
    uint32[] memory cData = new uint32[](5);
    cData[0] = stats.level;
    cData[1] = stats.experience;
    cData[2] = stats.life;
    cData[3] = stats.mana;
    cData[4] = stats.lifeChances;

    return cData.packUint32Array();
  }

  function unpackChangeableStats(bytes32 data) internal pure returns (IStatController.ChangeableStats memory result) {
    uint32[] memory cData = data.unpackUint32Array();
    return IStatController.ChangeableStats({
      level: cData[0],
      experience: cData[1],
      life: cData[2],
      mana: cData[3],
      lifeChances: cData[4]
    });
  }

  function bytesToFullAttributesArray(bytes32[] memory attributes) internal pure returns (int32[] memory result) {
    (int32[] memory values, uint8[] memory ids) = attributes.toInt32ArrayWithIds();
    return valuesToFullAttributesArray(values, ids);
  }

  function valuesToFullAttributesArray(int32[] memory values, uint8[] memory ids) internal pure returns (int32[] memory result) {
    result = new int32[](uint(IStatController.ATTRIBUTES.END_SLOT));
    for (uint i; i < values.length; ++i) {
      int32 value = values[i];
      if (value != 0) {
        result[ids[i]] = value;
      }
    }
  }
  //endregion --------------------------- CALCULATIONS

}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

import "../interfaces/IStoryController.sol";
import "../interfaces/IOracle.sol";
import "../interfaces/ITreasury.sol";
import "../interfaces/IGOC.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/IERC721Enumerable.sol";
import "../interfaces/IAppErrors.sol";
import "../interfaces/IApplicationEvents.sol";
import "../lib/CalcLib.sol";
import "../lib/PackingLib.sol";
import "../lib/ItemLib.sol";
import "../lib/StringLib.sol";

library StoryLib {
  using CalcLib for uint;
  using PackingLib for address;
  using PackingLib for uint16;
  using PackingLib for bytes32;
  using PackingLib for bytes32[];

  //region ------------------------ Constants
  /// @notice Max number of items that can be minted per iteration in the stories
  uint internal constant MAX_MINTED_ITEMS_PER_ITERATION = 3;
  //endregion ------------------------ Constants

  //region ------------------------ Story logic

  /// @notice Make action, increment STORY_XXX hero custom data if the dungeon is completed / hero is killed
  function action(IGOC.ActionContext memory ctx, uint16 storyId) internal returns (IGOC.ActionResult memory result) {
    if (storyId == 0) revert IAppErrors.ZeroStoryIdAction();

    result = IStoryController(ctx.controller.storyController()).storyAction(
      ctx.sender,
      ctx.dungeonId,
      ctx.objectId,
      ctx.stageId,
      ctx.heroToken,
      ctx.heroTokenId,
      ctx.biome,
      ctx.iteration,
      ctx.data
    );

    if (result.completed || result.kill) {
      IStatController statController = IStatController(ctx.controller.statController());
      bytes32 index = _getStoryIndex(storyId);
      uint curValue = statController.heroCustomData(ctx.heroToken, ctx.heroTokenId, index);
      statController.setHeroCustomData(ctx.heroToken, ctx.heroTokenId, index, curValue + 1);
    }
  }

  /// @notice Check if the story is available for the hero
  /// The story is available if hero level fits to requirements
  /// and if the hero/global custom data requirements are met (current value is inside of [min, max])
  function isStoryAvailableForHero(
    IStoryController.MainState storage s,
    uint16 storyId,
    address heroToken,
    uint heroTokenId,
    address statController
  ) internal view returns (bool) {
    uint reqLvl = s.storyRequiredLevel[storyId];
    if (reqLvl != 0 && IStatController(statController).heroStats(heroToken, heroTokenId).level < reqLvl) {
      return false;
    }

    IStoryController.CustomDataRequirementRangePacked[] storage allData = s.storyRequiredHeroData[storyId];
    uint len = allData.length;
    for (uint i; i < len; ++i) {
      IStoryController.CustomDataRequirementRangePacked memory data = allData[i];

      if (data.index == bytes32(0)) continue;

      (uint64 min, uint64 max, bool isHeroData) = data.data.unpackCustomDataRequirements();

      uint value = isHeroData
        ? IStatController(statController).heroCustomData(heroToken, heroTokenId, data.index)
        : IStatController(statController).globalCustomData(data.index);

      if (value < uint(min) || value > uint(max)) {
        return false;
      }
    }

    return true;
  }

  /// @notice Update bonus attributes, refresh hero states, initialize and return results
  /// @param mintItemsData Source for _mintRandomItems, random item (max 1, probably 0) is selected and put to results
  /// @param mintItems_ Function _mintRandomItems is passed here. Parameter is required to make unit tests.
  function handleResult(
    IStoryController.StoryActionContext memory context,
    bytes32[] memory attributesChanges,
    bytes32 statsChanges,
    bytes32[] memory mintItemsData,
    function (IStoryController.StoryActionContext memory, bytes32[] memory) internal returns (address[] memory) mintItems_
  ) internal returns (IGOC.ActionResult memory result) {
    result.heroToken = context.heroToken;
    result.heroTokenId = context.heroTokenId;
    result.objectId = context.objectId;

    int32[] memory attributes = _generateAttributes(attributesChanges);

    if (attributes.length != 0) {
      context.statController.changeBonusAttributes(IStatController.ChangeAttributesInfo({
        heroToken: context.heroToken,
        heroTokenId: context.heroTokenId,
        changeAttributes: attributes,
        add: true,
        temporally: true
      }));
      // changeBonusAttributes can change life and mana, so we need to refresh hero stats. It's safer to do it always
      context.heroStats = context.statController.heroStats(context.heroToken, context.heroTokenId);
      emit IApplicationEvents.StoryChangeAttributes(
        context.objectId,
        context.heroToken,
        context.heroTokenId,
        context.dungeonId,
        context.storyId,
        context.stageId,
        context.iteration,
        attributes
      );
    }

    IStoryController.StatsChange memory statsToChange = _generateStats(statsChanges);

    if (statsToChange.heal != 0) {
      int32 max = context.statController.heroAttribute(context.heroToken, context.heroTokenId, uint(IStatController.ATTRIBUTES.LIFE));
      result.heal = max * statsToChange.heal / 100;
    }

    if (statsToChange.manaRegen != 0) {
      int32 max = context.statController.heroAttribute(context.heroToken, context.heroTokenId, uint(IStatController.ATTRIBUTES.MANA));
      result.manaRegen = max * statsToChange.manaRegen / 100;
    }

    if (statsToChange.damage != 0) {
      int32 max = context.statController.heroAttribute(context.heroToken, context.heroTokenId, uint(IStatController.ATTRIBUTES.LIFE));
      result.damage = max * statsToChange.damage / 100;

      if (int32(context.heroStats.life) <= result.damage) {
        result.kill = true;
      }
    }

    if (statsToChange.manaConsumed != 0) {
      int32 max = context.statController.heroAttribute(context.heroToken, context.heroTokenId, uint(IStatController.ATTRIBUTES.MANA));
      result.manaConsumed = CalcLib.minI32(max * statsToChange.manaConsumed / 100, int32(context.heroStats.mana));
    }

    result.experience = statsToChange.experience;
    result.lifeChancesRecovered = statsToChange.lifeChancesRecovered;

    if (mintItemsData.length != 0) {
      result.mintItems = mintItems_(context, mintItemsData);
    }
    return result;
  }

  /// @notice Put data from {heroCustomDatas} and {globalCustomDatas} to {statController}
  function handleCustomDataResult(
    IStoryController.StoryActionContext memory context,
    bytes32[] memory heroCustomDatas,
    bytes32[] memory globalCustomDatas
  ) internal {
    uint len = heroCustomDatas.length;
    for (uint i; i < len; ++i) {

      (bytes32 customDataIndex, int16 value) = heroCustomDatas[i].unpackCustomDataChange();

      if (customDataIndex != 0) {
        uint curValue = context.statController.heroCustomData(context.heroToken, context.heroTokenId, customDataIndex);
        context.statController.setHeroCustomData(
          context.heroToken,
          context.heroTokenId,
          customDataIndex,
          value == 0
            ? 0
            : value > 0
              ? curValue + uint(int(value))
              : curValue.minusWithZeroFloor(uint(int(- value)))
        );
      }
    }

    len = globalCustomDatas.length;
    for (uint i; i < len; ++i) {

      (bytes32 customDataIndex, int16 value) = globalCustomDatas[i].unpackCustomDataChange();

      if (customDataIndex != 0) {
        uint curValue = context.statController.globalCustomData(customDataIndex);
        context.statController.setGlobalCustomData(
          customDataIndex,
          value == 0
            ? 0
            : value > 0
              ? curValue + uint(int(value))
              : curValue.minusWithZeroFloor(uint(int(- value)))
        );
      }
    }
  }

  /// @notice SIP-003: Randomly select one or several items, break them and increase their fragility by 1%.
  function breakItem(IStoryController.StoryActionContext memory context, IStoryController.MainState storage s) internal {
    bytes32[] storage breakInfos = s.burnItem[context.answerIdHash];
    uint length = breakInfos.length;

    for (uint i; i < length; ++i) {
      (uint8 slot, uint64 chance, bool stopIfBroken) = breakInfos[i].unpackBreakInfo();
      uint8[2] memory slots = _adjustSlotToBreak(slot, context.oracle);
      // Normally, {slots} contains two similar items and we need to check only first item.
      // But "hands" is a special case: TWO_HAND and RIGHT_HAND should be checked both independently => + cycle by k
      uint countSlots = slots[0] == slots[1] ? 1 : 2;

      for (uint k = 0; k < countSlots; ++k) {
        if (chance != 0 && context.oracle.getRandomNumberInRange(0, 100, 0) <= uint(chance)) {
          uint8[] memory busySlots = context.statController.heroItemSlots(context.heroToken, context.heroTokenId);

          uint lenBusySlots = busySlots.length;
          if (lenBusySlots != 0) {
            uint busySlotIndex;
            bool itemExist;
            if (slot == 0) {
              busySlotIndex = context.oracle.getRandomNumberInRange(0, lenBusySlots - 1, 0);
              itemExist = true;
            } else {
              for (uint j; j < lenBusySlots; ++j) {
                if (busySlots[j] == slots[k]) {
                  busySlotIndex = j;
                  itemExist = true;
                  break;
                }
              }
            }

            if (itemExist) {
              // SIP-003: don't burn item but break it
              _breakItemInHeroSlot(context, busySlots[busySlotIndex]);
              if (stopIfBroken) {
                return; // go out of two cycles
              }
            }
          }
        }
      }
    }
  }

  /// @notice SCB-1016. There are some slots with equal meaning:
  /// 1) weapon can be RIGHT-HAND, TWO-HAND (LEFT-HAND is not considered here)
  /// 2) ring can be LEFT, RIGHT
  /// 3) skill can be SKILL_1, SKILL_2, SKILL_3
  /// Story-writer is able to specify only one slot to break.
  /// 1) if ONE/TWO-HAND slot is specified then any available weapon (ONE or TWO hands) should be broken
  /// 2) if LEFT right is specified then random(LEFT or RIGHT) slot should be broken
  /// 3) skills - there is same rule as for the rings
  /// @return slots Slots that should be checked. Normally {slots} contains same item twice.
  /// The items are different in one case only: [RIGHT_HAND, TWO_HAND]
  function _adjustSlotToBreak(uint8 slot, IOracle oracle) internal returns (uint8[2] memory slots) {
    if (slot == uint8(IStatController.ItemSlots.RIGHT_HAND) || slot == uint8(IStatController.ItemSlots.TWO_HAND)) {
      return [uint8(IStatController.ItemSlots.RIGHT_HAND), uint8(IStatController.ItemSlots.TWO_HAND)];
    } else if (slot == uint8(IStatController.ItemSlots.RIGHT_RING) || slot == uint8(IStatController.ItemSlots.LEFT_RING)) {
      uint8 selectedSlot = (oracle.getRandomNumber(1, 0) == 0)
        ? uint8(IStatController.ItemSlots.RIGHT_RING)
        : uint8(IStatController.ItemSlots.LEFT_RING);
      return [selectedSlot, selectedSlot];
    } else if (
      slot == uint8(IStatController.ItemSlots.SKILL_1)
      || slot == uint8(IStatController.ItemSlots.SKILL_2)
      || slot == uint8(IStatController.ItemSlots.SKILL_3)
    ) {
      uint rnd = oracle.getRandomNumber(2, 0);
      uint8 selectedSlot = (rnd == 0)
        ? uint8(IStatController.ItemSlots.SKILL_1)
        : ((rnd == 1)
          ? uint8(IStatController.ItemSlots.SKILL_2)
          : uint8(IStatController.ItemSlots.SKILL_3));
      return [selectedSlot, selectedSlot];
    } else {
      return [slot, slot];
    }
  }

  /// @notice Update internal hero state, generate {result}
  /// @param context We update some fields in place, so memory, not calldata here
  function handleAnswer(
    IStoryController.AnswerResultId answerResultId,
    IStoryController.MainState storage s,
    IStoryController.StoryActionContext memory context
  ) external returns (
    IGOC.ActionResult memory result,
    uint16 nextPage,
    uint16[] memory nextPages
  ) {
    return _handleAnswer(answerResultId, s, context, _mintRandomItems);
  }

  /// @notice Update internal hero state, generate {result}
  /// @param context We update some fields in place, so memory, not calldata here
  /// @param mintItems_ Function _mintRandomItems is passed here. Parameter is required to make unit tests.
  function _handleAnswer(
    IStoryController.AnswerResultId answerResultId,
    IStoryController.MainState storage s,
    IStoryController.StoryActionContext memory context,
    function (IStoryController.StoryActionContext memory, bytes32[] memory) internal returns (address[] memory) mintItems_
  ) internal returns (
    IGOC.ActionResult memory result,
    uint16 nextPage,
    uint16[] memory nextPages
  ) {
    result.objectId = context.objectId;
    result.heroTokenId = context.heroTokenId;
    result.heroToken = context.heroToken;

    nextPages = s.nextPageIds[context.storyId.packStoryNextPagesId(
      context.pageId,
      context.heroClassFromAnswerHash,
      context.answerNumber,
      uint8(answerResultId)
    )];
    nextPage = _getNextPage(context.oracle, nextPages);

    // number of items that can be minted inside single iteration in the story is limited
    // if the max is reached the minting is silently skipped
    // we assume here, that mintItems_ mints only 1 item so it's not necessary to limit number of minted items inside mintItems_
    uint mintedInIteration = _getMintedInIteration(s, context);

    if (answerResultId == IStoryController.AnswerResultId.SUCCESS) {
      result = handleResult(
        context,
        s.successInfoAttributes[context.answerIdHash],
        s.successInfoStats[context.answerIdHash],
        mintedInIteration < MAX_MINTED_ITEMS_PER_ITERATION ? s.successInfoMintItems[context.answerIdHash] : new bytes32[](0),
        mintItems_
      );

      handleCustomDataResult(
        context,
        s.customDataResult[context.storyId.packStoryCustomDataResult(
          context.pageId,
          context.heroClassFromAnswerHash,
          context.answerNumber,
          uint8(IStoryController.CustomDataResult.HERO_SUCCESS)
        )],
        s.customDataResult[context.storyId.packStoryCustomDataResult(
          context.pageId,
          context.heroClassFromAnswerHash,
          context.answerNumber,
          uint8(IStoryController.CustomDataResult.GLOBAL_SUCCESS)
        )]
      );
    } else {
      result = handleResult(
        context,
        s.failInfoAttributes[context.answerIdHash],
        s.failInfoStats[context.answerIdHash],
        mintedInIteration < MAX_MINTED_ITEMS_PER_ITERATION ? s.failInfoMintItems[context.answerIdHash] : new bytes32[](0),
        mintItems_
      );

      handleCustomDataResult(
        context,
        s.customDataResult[context.storyId.packStoryCustomDataResult(
          context.pageId,
          context.heroClassFromAnswerHash,
          context.answerNumber,
          uint8(IStoryController.CustomDataResult.HERO_FAIL)
        )],
        s.customDataResult[context.storyId.packStoryCustomDataResult(
          context.pageId,
          context.heroClassFromAnswerHash,
          context.answerNumber,
          uint8(IStoryController.CustomDataResult.GLOBAL_FAIL)
        )]
      );
    }

    if (result.mintItems.length != 0) {
      _setMintedInIteration(s, context, mintedInIteration + result.mintItems.length);
    }
  }

  /// @notice Check if the user has already minted an item within the current iteration of the story.
  /// if the item is already minted any additional minting should be skipped without revert
  function _getMintedInIteration(IStoryController.MainState storage s, IStoryController.StoryActionContext memory context)
  internal view returns (uint countMintedItems) {
    return s.mintedInIteration[context.heroToken.packStoryHeroStateId(context.heroTokenId, context.storyId)][context.iteration];
  }

  /// @notice Mark that the user has already minted an item within the current iteration of the story
  /// Only minting of the single item is allowed per iteration
  function _setMintedInIteration(
    IStoryController.MainState storage s,
    IStoryController.StoryActionContext memory context,
    uint newCountMintedItems
  ) internal {
    s.mintedInIteration[context.heroToken.packStoryHeroStateId(context.heroTokenId, context.storyId)][context.iteration] = newCountMintedItems;
  }

  /// @notice Revert if {heroAnswers} doesn't contain {answerIdHash}
  function checkAnswerIndexValid(bytes32[] memory heroAnswers, bytes32 answerIdHash) internal pure {
    uint len = heroAnswers.length;
    for (uint i; i < len; ++i) {
      if (heroAnswers[i] == answerIdHash) return;
    }
    revert IAppErrors.NotAnswer();
  }

  /// @notice Clear heroState for the current story
  /// @return nextObjs Default nextObjectsRewrite for the current page (values for 0 hero class)
  function finishStory(IStoryController.StoryActionContext memory ctx, IStoryController.MainState storage s) internal returns (
    uint32[] memory nextObjs
  ) {
    delete s.heroState[ctx.heroToken.packStoryHeroStateId(ctx.heroTokenId, ctx.storyId)];
    // It's not necessary to clear mintedInIteration because for each hero each object has a sequence of iterations
    // that is not reset on changing dungeons

    return s.nextObjectsRewrite[ctx.storyId.packStoryPageId(ctx.pageId, 0)];
  }

  //endregion ------------------------ Story logic

  //region ------------------------ Internal utils for story logic

  /// @dev This function is made separate to simplify unit testing
  function _mintRandomItems(IStoryController.StoryActionContext memory context, bytes32[] memory mintItemsData) internal returns (
    address[] memory
  ) {
    uint len = mintItemsData.length;
    address[] memory mintItems = new address[](len);
    uint32[] memory mintItemsChances = new uint32[](len);
    for (uint i; i < len; ++i) {
      (mintItems[i], mintItemsChances[i]) = mintItemsData[i].unpackItemMintInfo();
    }

    return ItemLib.mintRandomItems(ItemLib.MintItemInfo({
      mintItems: mintItems,
      mintItemsChances: mintItemsChances,
      amplifier: 0,
      seed: 0,
      oracle: context.oracle,
      magicFind: 0,
      destroyItems: 0,
      maxItems: 1, // MINT ONLY 1 ITEM!
      mintDropChanceDelta: StatLib.mintDropChanceDelta(context.heroStats.experience, uint8(context.heroStats.level), context.biome),
      mintDropChanceNgLevelMultiplier: 1e18
    }));
  }

  /// @param attributesChanges Values+ids packed using toBytes32ArrayWithIds
  function _generateAttributes(bytes32[] memory attributesChanges) internal pure returns (int32[] memory attributes) {
    if (attributesChanges.length != 0) {
      (int32[] memory values, uint8[] memory ids) = attributesChanges.toInt32ArrayWithIds();
      uint len = ids.length;
      if (len != 0) {
        attributes = new int32[](uint(IStatController.ATTRIBUTES.END_SLOT));
        for (uint i; i < len; ++i) {
          int32 value = values[i];
          attributes[ids[i]] = value;
        }
      }
    }

    return attributes;
  }

  function _generateStats(bytes32 statsChanges) internal pure returns (IStoryController.StatsChange memory change) {
    (
      change.experience,
      change.heal,
      change.manaRegen,
      change.lifeChancesRecovered,
      change.damage,
      change.manaConsumed
    ) = statsChanges.unpackStatsChange();

    return change;
  }

  /// @notice Break the item from the given {slot} (i.e. reduce item's durability to 0) and take it off
  /// Broken item is taken off also.
  function _breakItemInHeroSlot(IStoryController.StoryActionContext memory ctx, uint8 slot) internal {
    (address itemAdr, uint itemId) = ctx.statController.heroItemSlot(ctx.heroToken, uint64(ctx.heroTokenId), slot).unpackNftId();

    // take off the broken item and mark it as broken
    ctx.itemController.takeOffDirectly(itemAdr, itemId, ctx.heroToken, ctx.heroTokenId, slot, ctx.sender, true);

    // add 1% of fragility, deprecated
    // ctx.itemController.incBrokenItemFragility(itemAdr, itemId);

    emit IApplicationEvents.ItemBroken(
      ctx.heroToken,
      ctx.heroTokenId,
      ctx.dungeonId,
      ctx.objectId,
      itemAdr,
      itemId,
      ctx.stageId,
      ctx.iteration
    );
  }

  function _getNextPage(IOracle oracle, uint16[] memory pages) internal returns (uint16) {
    if (pages.length == 0) {
      return 0;
    }
    if (pages.length == 1) {
      return pages[0];
    }
    return pages[oracle.getRandomNumberInRange(0, pages.length - 1, 0)];
  }

  function _getStoryIndex(uint16 storyId) internal pure returns (bytes32) {
    return bytes32(abi.encodePacked("STORY_", StringLib._toString(storyId)));
  }
  //endregion ------------------------ Internal utils for story logic

  //region ------------------------ Check answers

  function checkAnswer(
    IStoryController.StoryActionContext memory context,
    IStoryController.MainState storage s
  ) external returns (IStoryController.AnswerResultId result) {
    result = checkAnswerAttributes(context, context.answerIdHash, s);
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerItems(context, context.answerIdHash, s);
    }
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerTokens(context, context.answerIdHash, s);
    }
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerDelay(context);
    }
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerHeroCustomData(context, context.answerIdHash, s);
    }
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerGlobalCustomData(context, context.answerIdHash, s);
    }
    if (result == IStoryController.AnswerResultId.SUCCESS) {
      result = checkAnswerRandom(context);
    }
  }

  /// @notice Check if hero attribute values meet attribute requirements for the given answer
  function checkAnswerAttributes(
    IStoryController.StoryActionContext memory context,
    bytes32 answerIndex,
    IStoryController.MainState storage s
  ) internal view returns (IStoryController.AnswerResultId) {
    bytes32[] storage reqs = s.attributeRequirements[answerIndex];
    uint length = reqs.length;

    for (uint i; i < length; ++i) {
      (uint8 attributeIndex, int32 value, bool isCore) = reqs[i].unpackStoryAttributeRequirement();
      if (isCore) {
        IStatController.CoreAttributes memory base = context.statController.heroBaseAttributes(context.heroToken, context.heroTokenId);
        if (attributeIndex == uint8(IStatController.ATTRIBUTES.STRENGTH) && base.strength < value) {
          return IStoryController.AnswerResultId.ATTRIBUTE_FAIL;
        }
        if (attributeIndex == uint8(IStatController.ATTRIBUTES.DEXTERITY) && base.dexterity < value) {
          return IStoryController.AnswerResultId.ATTRIBUTE_FAIL;
        }
        if (attributeIndex == uint8(IStatController.ATTRIBUTES.VITALITY) && base.vitality < value) {
          return IStoryController.AnswerResultId.ATTRIBUTE_FAIL;
        }
        if (attributeIndex == uint8(IStatController.ATTRIBUTES.ENERGY) && base.energy < value) {
          return IStoryController.AnswerResultId.ATTRIBUTE_FAIL;
        }
      } else {
        int32 attr = context.statController.heroAttribute(context.heroToken, context.heroTokenId, attributeIndex);
        if (attr < value) {
          return IStoryController.AnswerResultId.ATTRIBUTE_FAIL;
        }
      }
    }

    return IStoryController.AnswerResultId.SUCCESS;
  }

  /// @notice Check item requirements for the given answer, check following issues:
  /// 1) For equipped item: check if it is on balance
  /// 2) For not equipped item: burn first owned item if requireItemBurn OR check that not equipped item is on balance
  function checkAnswerItems(
    IStoryController.StoryActionContext memory context,
    bytes32 answerIndex,
    IStoryController.MainState storage s
  ) internal returns (IStoryController.AnswerResultId) {

    bytes32[] storage reqs = s.itemRequirements[answerIndex];
    uint length = reqs.length;

    for (uint i; i < length; ++i) {
      (address item, bool requireItemBurn, bool requireItemEquipped) = reqs[i].unpackStoryItemRequirement();

      // equipped item is on balance of the heroToken, not on balance of the sender
      if (requireItemEquipped && IERC721Enumerable(item).balanceOf(context.heroToken) == 0) {
        revert IAppErrors.NotItem1();
      }

      if (requireItemBurn) {
        _burnFirstOwnedItem(context, item);
      }

      if (!requireItemEquipped && !requireItemBurn) {
        if (IERC721Enumerable(item).balanceOf(context.sender) == 0) revert IAppErrors.NotItem2();
      }

    }
    return IStoryController.AnswerResultId.SUCCESS;
  }

  /// @notice burn first owned item and generate event
  /// @dev Use separate function to workaround stack too deep
  function _burnFirstOwnedItem(IStoryController.StoryActionContext memory context, address item) internal {
    uint itemId = IERC721Enumerable(item).tokenOfOwnerByIndex(context.sender, 0);
    context.itemController.destroy(item, itemId); // destroy reverts if the item is equipped

    emit IApplicationEvents.NotEquippedItemBurned(
      context.heroToken,
      context.heroTokenId,
      context.dungeonId,
      context.storyId,
      item,
      itemId,
      context.stageId,
      context.iteration
    );
  }

  /// @notice Ensure that the sender has enough amounts of the required tokens, send fees to the treasury
  function checkAnswerTokens(
    IStoryController.StoryActionContext memory context,
    bytes32 answerIndex,
    IStoryController.MainState storage s
  ) internal returns (IStoryController.AnswerResultId) {
    bytes32[] memory reqs = s.tokenRequirements[answerIndex];
    uint length = reqs.length;
    for (uint i; i < length; ++i) {
      (address token, uint88 amount, bool requireTransfer) = reqs[i].unpackStoryTokenRequirement();
      amount = uint88(adjustTokenAmountToGameToken(uint(amount), context.controller));

      if (amount != 0) {
        uint balance = IERC20(token).balanceOf(context.sender);
        if (balance < uint(amount)) revert IAppErrors.NotEnoughAmount(balance, uint(amount));

        if (requireTransfer) {
          context.controller.process(token, amount, context.sender);
        }
      }
    }
    return IStoryController.AnswerResultId.SUCCESS;
  }

  function adjustTokenAmountToGameToken(uint amount, IController controller) internal view returns(uint) {
    return amount * controller.gameTokenPrice() / 1e18;
  }

  /// @notice Generate error randomly
  function checkAnswerRandom(IStoryController.StoryActionContext memory context) internal returns (IStoryController.AnswerResultId) {
    (uint32 random,,) = context.answerAttributes.unpackStorySimpleRequirement();

    if (random != 0 && random < 100) {
      if (context.oracle.getRandomNumber(100, 0) > uint(random)) {
        return IStoryController.AnswerResultId.RANDOM_FAIL;
      }
    } else if (random > 100) {
      revert IAppErrors.NotRandom(random);
    }

    return IStoryController.AnswerResultId.SUCCESS;
  }

  /// @notice Ensure that the answer was given fast enough
  function checkAnswerDelay(IStoryController.StoryActionContext memory context) internal view returns (IStoryController.AnswerResultId) {

    (,uint32 delay,) = context.answerAttributes.unpackStorySimpleRequirement();

    if (delay != 0) {
      uint lastCall = uint(context.heroLastActionTS);
      if (lastCall != 0 && lastCall < block.timestamp && block.timestamp - lastCall > uint(delay)) {
        return IStoryController.AnswerResultId.DELAY_FAIL;
      }
    }

    return IStoryController.AnswerResultId.SUCCESS;
  }

  function checkAnswerHeroCustomData(
    IStoryController.StoryActionContext memory context,
    bytes32 answerIndex,
    IStoryController.MainState storage s
  ) internal view returns (IStoryController.AnswerResultId) {
    return _checkAnswerCustomData(context, s.heroCustomDataRequirement[answerIndex], true);
  }

  function checkAnswerGlobalCustomData(
    IStoryController.StoryActionContext memory context,
    bytes32 answerIndex,
    IStoryController.MainState storage s
  ) internal view returns (IStoryController.AnswerResultId) {
    return _checkAnswerCustomData(context, s.globalCustomDataRequirement[answerIndex], false);
  }

  function _checkAnswerCustomData(
    IStoryController.StoryActionContext memory context,
    IStoryController.CustomDataRequirementPacked[] memory datas,
    bool heroCustomData
  ) internal view returns (IStoryController.AnswerResultId) {
    uint len = datas.length;
    for (uint i; i < len; ++i) {
      IStoryController.CustomDataRequirementPacked memory data = datas[i];

      if (data.index != 0) {
        (uint valueMin, uint valueMax, bool mandatory) = data.data.unpackCustomDataRequirements();
        uint heroValue = heroCustomData
          ? context.statController.heroCustomData(context.heroToken, context.heroTokenId, data.index)
          : context.statController.globalCustomData(data.index);

        if (heroValue < valueMin || heroValue > valueMax) {
          if (mandatory) {
            if (heroCustomData) {
              revert IAppErrors.NotHeroData();
            } else {
              revert IAppErrors.NotGlobalData();
            }
          } else {
            return heroCustomData
              ? IStoryController.AnswerResultId.HERO_CUSTOM_DATA_FAIL
              : IStoryController.AnswerResultId.GLOBAL_CUSTOM_DATA_FAIL;
          }
        }
      }
    }

    return IStoryController.AnswerResultId.SUCCESS;
  }

  //endregion ------------------------ Check answers
}

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;


library StringLib {

  /// @dev Inspired by OraclizeAPI's implementation - MIT license
  ///      https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
  function toString(uint value) external pure returns (string memory) {
    return _toString(value);
  }

  function _toString(uint value) internal pure returns (string memory) {
    if (value == 0) {
      return "0";
    }
    uint temp = value;
    uint digits;
    while (temp != 0) {
      digits++;
      temp /= 10;
    }
    bytes memory buffer = new bytes(digits);
    while (value != 0) {
      digits -= 1;
      buffer[digits] = bytes1(uint8(48 + uint(value % 10)));
      value /= 10;
    }
    return string(buffer);
  }

  function toAsciiString(address x) external pure returns (string memory) {
    return _toAsciiString(x);
  }

  function _toAsciiString(address x) internal pure returns (string memory) {
    bytes memory s = new bytes(40);
    for (uint i = 0; i < 20; i++) {
      bytes1 b = bytes1(uint8(uint(uint160(x)) / (2 ** (8 * (19 - i)))));
      bytes1 hi = bytes1(uint8(b) / 16);
      bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
      s[2 * i] = _char(hi);
      s[2 * i + 1] = _char(lo);
    }
    return string(s);
  }

  function char(bytes1 b) external pure returns (bytes1 c) {
    return _char(b);
  }

  function _char(bytes1 b) internal pure returns (bytes1 c) {
    if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
    else return bytes1(uint8(b) + 0x57);
  }

  function concat(string memory a, string memory b) internal pure returns (string memory) {
    return string(abi.encodePacked(a, b));
  }

  function isASCIILettersOnly(string memory str) internal pure returns (bool) {
    bytes memory b = bytes(str);
    for (uint i = 0; i < b.length; i++) {
      if (uint8(b[i]) < 32 || uint8(b[i]) > 127) {
        return false;
      }
    }
    return true;
  }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableMap.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableMap.js.

pragma solidity ^0.8.20;

import {EnumerableSet} from "./EnumerableSet.sol";

/**
 * @dev Library for managing an enumerable variant of Solidity's
 * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`]
 * type.
 *
 * Maps have the following properties:
 *
 * - Entries are added, removed, and checked for existence in constant time
 * (O(1)).
 * - Entries are enumerated in O(n). No guarantees are made on the ordering.
 *
 * ```solidity
 * contract Example {
 *     // Add the library methods
 *     using EnumerableMap for EnumerableMap.UintToAddressMap;
 *
 *     // Declare a set state variable
 *     EnumerableMap.UintToAddressMap private myMap;
 * }
 * ```
 *
 * The following map types are supported:
 *
 * - `uint256 -> address` (`UintToAddressMap`) since v3.0.0
 * - `address -> uint256` (`AddressToUintMap`) since v4.6.0
 * - `bytes32 -> bytes32` (`Bytes32ToBytes32Map`) since v4.6.0
 * - `uint256 -> uint256` (`UintToUintMap`) since v4.7.0
 * - `bytes32 -> uint256` (`Bytes32ToUintMap`) since v4.7.0
 *
 * [WARNING]
 * ====
 * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
 * unusable.
 * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
 *
 * In order to clean an EnumerableMap, you can either remove all elements one by one or create a fresh instance using an
 * array of EnumerableMap.
 * ====
 */
library EnumerableMap {
  using EnumerableSet for EnumerableSet.Bytes32Set;

  // To implement this library for multiple types with as little code repetition as possible, we write it in
  // terms of a generic Map type with bytes32 keys and values. The Map implementation uses private functions,
  // and user-facing implementations such as `UintToAddressMap` are just wrappers around the underlying Map.
  // This means that we can only create new EnumerableMaps for types that fit in bytes32.

  /**
   * @dev Query for a nonexistent map key.
     */
  error EnumerableMapNonexistentKey(bytes32 key);

  struct Bytes32ToBytes32Map {
    // Storage of keys
    EnumerableSet.Bytes32Set _keys;
    mapping(bytes32 key => bytes32) _values;
  }

  /**
   * @dev Adds a key-value pair to a map, or updates the value for an existing
     * key. O(1).
     *
     * Returns true if the key was added to the map, that is if it was not
     * already present.
     */
  function set(Bytes32ToBytes32Map storage map, bytes32 key, bytes32 value) internal returns (bool) {
    map._values[key] = value;
    return map._keys.add(key);
  }

  /**
   * @dev Removes a key-value pair from a map. O(1).
     *
     * Returns true if the key was removed from the map, that is if it was present.
     */
  function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) {
    delete map._values[key];
    return map._keys.remove(key);
  }

  /**
   * @dev Returns true if the key is in the map. O(1).
     */
  function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) {
    return map._keys.contains(key);
  }

  /**
   * @dev Returns the number of key-value pairs in the map. O(1).
     */
  function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) {
    return map._keys.length();
  }

  /**
   * @dev Returns the key-value pair stored at position `index` in the map. O(1).
     *
     * Note that there are no guarantees on the ordering of entries inside the
     * array, and it may change when more entries are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) {
    bytes32 key = map._keys.at(index);
    return (key, map._values[key]);
  }

  /**
   * @dev Tries to returns the value associated with `key`. O(1).
     * Does not revert if `key` is not in the map.
     */
  function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) {
    bytes32 value = map._values[key];
    if (value == bytes32(0)) {
      return (contains(map, key), bytes32(0));
    } else {
      return (true, value);
    }
  }

  /**
   * @dev Returns the value associated with `key`. O(1).
     *
     * Requirements:
     *
     * - `key` must be in the map.
     */
  function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) {
    bytes32 value = map._values[key];
    if (value == 0 && !contains(map, key)) {
      revert EnumerableMapNonexistentKey(key);
    }
    return value;
  }

  /**
   * @dev Return the an array containing all the keys
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function keys(Bytes32ToBytes32Map storage map) internal view returns (bytes32[] memory) {
    return map._keys.values();
  }

  // UintToUintMap

  struct UintToUintMap {
    Bytes32ToBytes32Map _inner;
  }

  /**
   * @dev Adds a key-value pair to a map, or updates the value for an existing
     * key. O(1).
     *
     * Returns true if the key was added to the map, that is if it was not
     * already present.
     */
  function set(UintToUintMap storage map, uint256 key, uint256 value) internal returns (bool) {
    return set(map._inner, bytes32(key), bytes32(value));
  }

  /**
   * @dev Removes a value from a map. O(1).
     *
     * Returns true if the key was removed from the map, that is if it was present.
     */
  function remove(UintToUintMap storage map, uint256 key) internal returns (bool) {
    return remove(map._inner, bytes32(key));
  }

  /**
   * @dev Returns true if the key is in the map. O(1).
     */
  function contains(UintToUintMap storage map, uint256 key) internal view returns (bool) {
    return contains(map._inner, bytes32(key));
  }

  /**
   * @dev Returns the number of elements in the map. O(1).
     */
  function length(UintToUintMap storage map) internal view returns (uint256) {
    return length(map._inner);
  }

  /**
   * @dev Returns the element stored at position `index` in the map. O(1).
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(UintToUintMap storage map, uint256 index) internal view returns (uint256, uint256) {
    (bytes32 key, bytes32 value) = at(map._inner, index);
    return (uint256(key), uint256(value));
  }

  /**
   * @dev Tries to returns the value associated with `key`. O(1).
     * Does not revert if `key` is not in the map.
     */
  function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) {
    (bool success, bytes32 value) = tryGet(map._inner, bytes32(key));
    return (success, uint256(value));
  }

  /**
   * @dev Returns the value associated with `key`. O(1).
     *
     * Requirements:
     *
     * - `key` must be in the map.
     */
  function get(UintToUintMap storage map, uint256 key) internal view returns (uint256) {
    return uint256(get(map._inner, bytes32(key)));
  }

  /**
   * @dev Return the an array containing all the keys
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function keys(UintToUintMap storage map) internal view returns (uint256[] memory) {
    bytes32[] memory store = keys(map._inner);
    uint256[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }

  // UintToAddressMap

  struct UintToAddressMap {
    Bytes32ToBytes32Map _inner;
  }

  /**
   * @dev Adds a key-value pair to a map, or updates the value for an existing
     * key. O(1).
     *
     * Returns true if the key was added to the map, that is if it was not
     * already present.
     */
  function set(UintToAddressMap storage map, uint256 key, address value) internal returns (bool) {
    return set(map._inner, bytes32(key), bytes32(uint256(uint160(value))));
  }

  /**
   * @dev Removes a value from a map. O(1).
     *
     * Returns true if the key was removed from the map, that is if it was present.
     */
  function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) {
    return remove(map._inner, bytes32(key));
  }

  /**
   * @dev Returns true if the key is in the map. O(1).
     */
  function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) {
    return contains(map._inner, bytes32(key));
  }

  /**
   * @dev Returns the number of elements in the map. O(1).
     */
  function length(UintToAddressMap storage map) internal view returns (uint256) {
    return length(map._inner);
  }

  /**
   * @dev Returns the element stored at position `index` in the map. O(1).
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) {
    (bytes32 key, bytes32 value) = at(map._inner, index);
    return (uint256(key), address(uint160(uint256(value))));
  }

  /**
   * @dev Tries to returns the value associated with `key`. O(1).
     * Does not revert if `key` is not in the map.
     */
  function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) {
    (bool success, bytes32 value) = tryGet(map._inner, bytes32(key));
    return (success, address(uint160(uint256(value))));
  }

  /**
   * @dev Returns the value associated with `key`. O(1).
     *
     * Requirements:
     *
     * - `key` must be in the map.
     */
  function get(UintToAddressMap storage map, uint256 key) internal view returns (address) {
    return address(uint160(uint256(get(map._inner, bytes32(key)))));
  }

  /**
   * @dev Return the an array containing all the keys
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function keys(UintToAddressMap storage map) internal view returns (uint256[] memory) {
    bytes32[] memory store = keys(map._inner);
    uint256[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }

  // AddressToUintMap

  struct AddressToUintMap {
    Bytes32ToBytes32Map _inner;
  }

  /**
   * @dev Adds a key-value pair to a map, or updates the value for an existing
     * key. O(1).
     *
     * Returns true if the key was added to the map, that is if it was not
     * already present.
     */
  function set(AddressToUintMap storage map, address key, uint256 value) internal returns (bool) {
    return set(map._inner, bytes32(uint256(uint160(key))), bytes32(value));
  }

  /**
   * @dev Removes a value from a map. O(1).
     *
     * Returns true if the key was removed from the map, that is if it was present.
     */
  function remove(AddressToUintMap storage map, address key) internal returns (bool) {
    return remove(map._inner, bytes32(uint256(uint160(key))));
  }

  /**
   * @dev Returns true if the key is in the map. O(1).
     */
  function contains(AddressToUintMap storage map, address key) internal view returns (bool) {
    return contains(map._inner, bytes32(uint256(uint160(key))));
  }

  /**
   * @dev Returns the number of elements in the map. O(1).
     */
  function length(AddressToUintMap storage map) internal view returns (uint256) {
    return length(map._inner);
  }

  /**
   * @dev Returns the element stored at position `index` in the map. O(1).
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) {
    (bytes32 key, bytes32 value) = at(map._inner, index);
    return (address(uint160(uint256(key))), uint256(value));
  }

  /**
   * @dev Tries to returns the value associated with `key`. O(1).
     * Does not revert if `key` is not in the map.
     */
  function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) {
    (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key))));
    return (success, uint256(value));
  }

  /**
   * @dev Returns the value associated with `key`. O(1).
     *
     * Requirements:
     *
     * - `key` must be in the map.
     */
  function get(AddressToUintMap storage map, address key) internal view returns (uint256) {
    return uint256(get(map._inner, bytes32(uint256(uint160(key)))));
  }

  /**
   * @dev Return the an array containing all the keys
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function keys(AddressToUintMap storage map) internal view returns (address[] memory) {
    bytes32[] memory store = keys(map._inner);
    address[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }

  // Bytes32ToUintMap

  struct Bytes32ToUintMap {
    Bytes32ToBytes32Map _inner;
  }

  /**
   * @dev Adds a key-value pair to a map, or updates the value for an existing
     * key. O(1).
     *
     * Returns true if the key was added to the map, that is if it was not
     * already present.
     */
  function set(Bytes32ToUintMap storage map, bytes32 key, uint256 value) internal returns (bool) {
    return set(map._inner, key, bytes32(value));
  }

  /**
   * @dev Removes a value from a map. O(1).
     *
     * Returns true if the key was removed from the map, that is if it was present.
     */
  function remove(Bytes32ToUintMap storage map, bytes32 key) internal returns (bool) {
    return remove(map._inner, key);
  }

  /**
   * @dev Returns true if the key is in the map. O(1).
     */
  function contains(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool) {
    return contains(map._inner, key);
  }

  /**
   * @dev Returns the number of elements in the map. O(1).
     */
  function length(Bytes32ToUintMap storage map) internal view returns (uint256) {
    return length(map._inner);
  }

  /**
   * @dev Returns the element stored at position `index` in the map. O(1).
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32, uint256) {
    (bytes32 key, bytes32 value) = at(map._inner, index);
    return (key, uint256(value));
  }

  /**
   * @dev Tries to returns the value associated with `key`. O(1).
     * Does not revert if `key` is not in the map.
     */
  function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) {
    (bool success, bytes32 value) = tryGet(map._inner, key);
    return (success, uint256(value));
  }

  /**
   * @dev Returns the value associated with `key`. O(1).
     *
     * Requirements:
     *
     * - `key` must be in the map.
     */
  function get(Bytes32ToUintMap storage map, bytes32 key) internal view returns (uint256) {
    return uint256(get(map._inner, key));
  }

  /**
   * @dev Return the an array containing all the keys
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the map grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function keys(Bytes32ToUintMap storage map) internal view returns (bytes32[] memory) {
    bytes32[] memory store = keys(map._inner);
    bytes32[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol)
// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.

pragma solidity ^0.8.20;

/**
 * @dev Library for managing
 * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
 * types.
 *
 * Sets have the following properties:
 *
 * - Elements are added, removed, and checked for existence in constant time
 * (O(1)).
 * - Elements are enumerated in O(n). No guarantees are made on the ordering.
 *
 * ```solidity
 * contract Example {
 *     // Add the library methods
 *     using EnumerableSet for EnumerableSet.AddressSet;
 *
 *     // Declare a set state variable
 *     EnumerableSet.AddressSet private mySet;
 * }
 * ```
 *
 * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
 * and `uint256` (`UintSet`) are supported.
 *
 * [WARNING]
 * ====
 * Trying to delete such a structure from storage will likely result in data corruption, rendering the structure
 * unusable.
 * See https://github.com/ethereum/solidity/pull/11843[ethereum/solidity#11843] for more info.
 *
 * In order to clean an EnumerableSet, you can either remove all elements one by one or create a fresh instance using an
 * array of EnumerableSet.
 * ====
 */
library EnumerableSet {
  // To implement this library for multiple types with as little code
  // repetition as possible, we write it in terms of a generic Set type with
  // bytes32 values.
  // The Set implementation uses private functions, and user-facing
  // implementations (such as AddressSet) are just wrappers around the
  // underlying Set.
  // This means that we can only create new EnumerableSets for types that fit
  // in bytes32.

  struct Set {
    // Storage of set values
    bytes32[] _values;
    // Position is the index of the value in the `values` array plus 1.
    // Position 0 is used to mean a value is not in the set.
    mapping(bytes32 value => uint256) _positions;
  }

  /**
   * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
  function _add(Set storage set, bytes32 value) private returns (bool) {
    if (!_contains(set, value)) {
      set._values.push(value);
      // The value is stored at length-1, but we add 1 to all indexes
      // and use 0 as a sentinel value
      set._positions[value] = set._values.length;
      return true;
    } else {
      return false;
    }
  }

  /**
   * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
  function _remove(Set storage set, bytes32 value) private returns (bool) {
    // We cache the value's position to prevent multiple reads from the same storage slot
    uint256 position = set._positions[value];

    if (position != 0) {
      // Equivalent to contains(set, value)
      // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
      // the array, and then remove the last element (sometimes called as 'swap and pop').
      // This modifies the order of the array, as noted in {at}.

      uint256 valueIndex = position - 1;
      uint256 lastIndex = set._values.length - 1;

      if (valueIndex != lastIndex) {
        bytes32 lastValue = set._values[lastIndex];

        // Move the lastValue to the index where the value to delete is
        set._values[valueIndex] = lastValue;
        // Update the tracked position of the lastValue (that was just moved)
        set._positions[lastValue] = position;
      }

      // Delete the slot where the moved value was stored
      set._values.pop();

      // Delete the tracked position for the deleted slot
      delete set._positions[value];

      return true;
    } else {
      return false;
    }
  }

  /**
   * @dev Returns true if the value is in the set. O(1).
     */
  function _contains(Set storage set, bytes32 value) private view returns (bool) {
    return set._positions[value] != 0;
  }

  /**
   * @dev Returns the number of values on the set. O(1).
     */
  function _length(Set storage set) private view returns (uint256) {
    return set._values.length;
  }

  /**
   * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function _at(Set storage set, uint256 index) private view returns (bytes32) {
    return set._values[index];
  }

  /**
   * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function _values(Set storage set) private view returns (bytes32[] memory) {
    return set._values;
  }

  // Bytes32Set

  struct Bytes32Set {
    Set _inner;
  }

  /**
   * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
  function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
    return _add(set._inner, value);
  }

  /**
   * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
  function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
    return _remove(set._inner, value);
  }

  /**
   * @dev Returns true if the value is in the set. O(1).
     */
  function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
    return _contains(set._inner, value);
  }

  /**
   * @dev Returns the number of values in the set. O(1).
     */
  function length(Bytes32Set storage set) internal view returns (uint256) {
    return _length(set._inner);
  }

  /**
   * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
    return _at(set._inner, index);
  }

  /**
   * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
    bytes32[] memory store = _values(set._inner);
    bytes32[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }

  // AddressSet

  struct AddressSet {
    Set _inner;
  }

  /**
   * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
  function add(AddressSet storage set, address value) internal returns (bool) {
    return _add(set._inner, bytes32(uint256(uint160(value))));
  }

  /**
   * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
  function remove(AddressSet storage set, address value) internal returns (bool) {
    return _remove(set._inner, bytes32(uint256(uint160(value))));
  }

  /**
   * @dev Returns true if the value is in the set. O(1).
     */
  function contains(AddressSet storage set, address value) internal view returns (bool) {
    return _contains(set._inner, bytes32(uint256(uint160(value))));
  }

  /**
   * @dev Returns the number of values in the set. O(1).
     */
  function length(AddressSet storage set) internal view returns (uint256) {
    return _length(set._inner);
  }

  /**
   * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(AddressSet storage set, uint256 index) internal view returns (address) {
    return address(uint160(uint256(_at(set._inner, index))));
  }

  /**
   * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function values(AddressSet storage set) internal view returns (address[] memory) {
    bytes32[] memory store = _values(set._inner);
    address[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }

  // UintSet

  struct UintSet {
    Set _inner;
  }

  /**
   * @dev Add a value to a set. O(1).
     *
     * Returns true if the value was added to the set, that is if it was not
     * already present.
     */
  function add(UintSet storage set, uint256 value) internal returns (bool) {
    return _add(set._inner, bytes32(value));
  }

  /**
   * @dev Removes a value from a set. O(1).
     *
     * Returns true if the value was removed from the set, that is if it was
     * present.
     */
  function remove(UintSet storage set, uint256 value) internal returns (bool) {
    return _remove(set._inner, bytes32(value));
  }

  /**
   * @dev Returns true if the value is in the set. O(1).
     */
  function contains(UintSet storage set, uint256 value) internal view returns (bool) {
    return _contains(set._inner, bytes32(value));
  }

  /**
   * @dev Returns the number of values in the set. O(1).
     */
  function length(UintSet storage set) internal view returns (uint256) {
    return _length(set._inner);
  }

  /**
   * @dev Returns the value stored at position `index` in the set. O(1).
     *
     * Note that there are no guarantees on the ordering of values inside the
     * array, and it may change when more values are added or removed.
     *
     * Requirements:
     *
     * - `index` must be strictly less than {length}.
     */
  function at(UintSet storage set, uint256 index) internal view returns (uint256) {
    return uint256(_at(set._inner, index));
  }

  /**
   * @dev Return the entire set in an array
     *
     * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
     * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
     * this function has an unbounded cost, and using it as part of a state-changing function may render the function
     * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
     */
  function values(UintSet storage set) internal view returns (uint256[] memory) {
    bytes32[] memory store = _values(set._inner);
    uint256[] memory result;

    /// @solidity memory-safe-assembly
    assembly {
      result := store
    }

    return result;
  }
}

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol)

pragma solidity ^0.8.20;

/**
 * @dev Standard math utilities missing in the Solidity language.
 */
library Math {

  /**
    * @dev Muldiv operation overflow.
   */
  error MathOverflowedMulDiv();

  enum Rounding {
    Floor, // Toward negative infinity
    Ceil, // Toward positive infinity
    Trunc, // Toward zero
    Expand // Away from zero
  }

  /**
   * @dev Returns the addition of two unsigned integers, with an success flag (no overflow).
     */
  function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
    unchecked {
      uint256 c = a + b;
      if (c < a) return (false, 0);
      return (true, c);
    }
  }

  /**
   * @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow).
     */
  function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
    unchecked {
      if (b > a) return (false, 0);
      return (true, a - b);
    }
  }

  /**
   * @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow).
     */
  function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
    unchecked {
    // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
    // benefit is lost if 'b' is also tested.
    // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
      if (a == 0) return (true, 0);
      uint256 c = a * b;
      if (c / a != b) return (false, 0);
      return (true, c);
    }
  }

  /**
   * @dev Returns the division of two unsigned integers, with a success flag (no division by zero).
     */
  function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
    unchecked {
      if (b == 0) return (false, 0);
      return (true, a / b);
    }
  }

  /**
   * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero).
     */
  function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
    unchecked {
      if (b == 0) return (false, 0);
      return (true, a % b);
    }
  }

  /**
   * @dev Returns the largest of two numbers.
     */
  function max(uint256 a, uint256 b) internal pure returns (uint256) {
    return a > b ? a : b;
  }

  /**
   * @dev Returns the smallest of two numbers.
     */
  function min(uint256 a, uint256 b) internal pure returns (uint256) {
    return a < b ? a : b;
  }

  /**
   * @dev Returns the average of two numbers. The result is rounded towards
     * zero.
     */
  function average(uint256 a, uint256 b) internal pure returns (uint256) {
    // (a + b) / 2 can overflow.
    return (a & b) + (a ^ b) / 2;
  }

  /**
   * @dev Returns the ceiling of the division of two numbers.
     *
     * This differs from standard division with `/` in that it rounds towards infinity instead
     * of rounding towards zero.
     */
  function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
    if (b == 0) {
      // Guarantee the same behavior as in a regular Solidity division.
      return a / b;
    }

    // The following calculation ensures accurate ceiling division without overflow.
    // Since a is non-zero, (a - 1) / b will not overflow.
    // The largest possible result occurs when (a - 1) / b is type(uint256).max,
    // but the largest value we can obtain is type(uint256).max - 1, which happens
    // when a = type(uint256).max and b = 1.
    unchecked {
      return a == 0 ? 0 : (a - 1) / b + 1;
    }
  }

  /**
   * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or
     * denominator == 0.
     * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by
     * Uniswap Labs also under MIT license.
     */
  function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
    unchecked {
    // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
    // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
    // variables such that product = prod1 * 2^256 + prod0.
      uint256 prod0 = x * y; // Least significant 256 bits of the product
      uint256 prod1; // Most significant 256 bits of the product
      assembly {
        let mm := mulmod(x, y, not(0))
        prod1 := sub(sub(mm, prod0), lt(mm, prod0))
      }

    // Handle non-overflow cases, 256 by 256 division.
      if (prod1 == 0) {
        // Solidity will revert if denominator == 0, unlike the div opcode on its own.
        // The surrounding unchecked block does not change this fact.
        // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
        return prod0 / denominator;
      }

    // Make sure the result is less than 2^256. Also prevents denominator == 0.
      if (denominator <= prod1) {
        revert MathOverflowedMulDiv();
      }

    ///////////////////////////////////////////////
    // 512 by 256 division.
    ///////////////////////////////////////////////

    // Make division exact by subtracting the remainder from [prod1 prod0].
      uint256 remainder;
      assembly {
      // Compute remainder using mulmod.
        remainder := mulmod(x, y, denominator)

      // Subtract 256 bit number from 512 bit number.
        prod1 := sub(prod1, gt(remainder, prod0))
        prod0 := sub(prod0, remainder)
      }

    // Factor powers of two out of denominator and compute largest power of two divisor of denominator.
    // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.

      uint256 twos = denominator & (0 - denominator);
      assembly {
      // Divide denominator by twos.
        denominator := div(denominator, twos)

      // Divide [prod1 prod0] by twos.
        prod0 := div(prod0, twos)

      // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
        twos := add(div(sub(0, twos), twos), 1)
      }

    // Shift in bits from prod1 into prod0.
      prod0 |= prod1 * twos;

    // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
    // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
    // four bits. That is, denominator * inv = 1 mod 2^4.
      uint256 inverse = (3 * denominator) ^ 2;

    // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also
    // works in modular arithmetic, doubling the correct bits in each step.
      inverse *= 2 - denominator * inverse; // inverse mod 2^8
      inverse *= 2 - denominator * inverse; // inverse mod 2^16
      inverse *= 2 - denominator * inverse; // inverse mod 2^32
      inverse *= 2 - denominator * inverse; // inverse mod 2^64
      inverse *= 2 - denominator * inverse; // inverse mod 2^128
      inverse *= 2 - denominator * inverse; // inverse mod 2^256

    // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
    // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
    // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
    // is no longer required.
      result = prod0 * inverse;
      return result;
    }
  }

  /**
   * @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
     */
  function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) {
    uint256 result = mulDiv(x, y, denominator);
    if (unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0) {
      result += 1;
    }
    return result;
  }

  /**
   * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
     * towards zero.
     *
     * Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
     */
  function sqrt(uint256 a) internal pure returns (uint256) {
    if (a == 0) {
      return 0;
    }

    // For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
    //
    // We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
    // `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
    //
    // This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
    // → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
    // → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
    //
    // Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
    uint256 result = 1 << (log2(a) >> 1);

    // At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
    // since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
    // every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
    // into the expected uint128 result.
    unchecked {
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      result = (result + a / result) >> 1;
      return min(result, a / result);
    }
  }

  /**
   * @notice Calculates sqrt(a), following the selected rounding direction.
     */
  function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
    unchecked {
      uint256 result = sqrt(a);
      return result + (unsignedRoundsUp(rounding) && result * result < a ? 1 : 0);
    }
  }

  /**
   * @dev Return the log in base 2 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     */
  function log2(uint256 value) internal pure returns (uint256) {
    uint256 result = 0;
    unchecked {
      if (value >> 128 > 0) {
        value >>= 128;
        result += 128;
      }
      if (value >> 64 > 0) {
        value >>= 64;
        result += 64;
      }
      if (value >> 32 > 0) {
        value >>= 32;
        result += 32;
      }
      if (value >> 16 > 0) {
        value >>= 16;
        result += 16;
      }
      if (value >> 8 > 0) {
        value >>= 8;
        result += 8;
      }
      if (value >> 4 > 0) {
        value >>= 4;
        result += 4;
      }
      if (value >> 2 > 0) {
        value >>= 2;
        result += 2;
      }
      if (value >> 1 > 0) {
        result += 1;
      }
    }
    return result;
  }

  /**
   * @dev Return the log in base 2, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
  function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
    unchecked {
      uint256 result = log2(value);
      return result + (unsignedRoundsUp(rounding) && 1 << result < value ? 1 : 0);
    }
  }

  /**
   * @dev Return the log in base 10 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     */
  function log10(uint256 value) internal pure returns (uint256) {
    uint256 result = 0;
    unchecked {
      if (value >= 10 ** 64) {
        value /= 10 ** 64;
        result += 64;
      }
      if (value >= 10 ** 32) {
        value /= 10 ** 32;
        result += 32;
      }
      if (value >= 10 ** 16) {
        value /= 10 ** 16;
        result += 16;
      }
      if (value >= 10 ** 8) {
        value /= 10 ** 8;
        result += 8;
      }
      if (value >= 10 ** 4) {
        value /= 10 ** 4;
        result += 4;
      }
      if (value >= 10 ** 2) {
        value /= 10 ** 2;
        result += 2;
      }
      if (value >= 10 ** 1) {
        result += 1;
      }
    }
    return result;
  }

  /**
   * @dev Return the log in base 10, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
  function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
    unchecked {
      uint256 result = log10(value);
      return result + (unsignedRoundsUp(rounding) && 10 ** result < value ? 1 : 0);
    }
  }

  /**
   * @dev Return the log in base 256 of a positive value rounded towards zero.
     * Returns 0 if given 0.
     *
     * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
     */
  function log256(uint256 value) internal pure returns (uint256) {
    uint256 result = 0;
    unchecked {
      if (value >> 128 > 0) {
        value >>= 128;
        result += 16;
      }
      if (value >> 64 > 0) {
        value >>= 64;
        result += 8;
      }
      if (value >> 32 > 0) {
        value >>= 32;
        result += 4;
      }
      if (value >> 16 > 0) {
        value >>= 16;
        result += 2;
      }
      if (value >> 8 > 0) {
        result += 1;
      }
    }
    return result;
  }

  /**
   * @dev Return the log in base 256, following the selected rounding direction, of a positive value.
     * Returns 0 if given 0.
     */
  function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
    unchecked {
      uint256 result = log256(value);
      return result + (unsignedRoundsUp(rounding) && 1 << (result << 3) < value ? 1 : 0);
    }
  }

  /**
   * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers.
     */
  function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) {
    return uint8(rounding) % 2 == 1;
  }

}

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Library for generating pseudorandom numbers.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/LibPRNG.sol)
/// @author LazyShuffler based on NextShuffler by aschlosberg (divergencearran)
/// (https://github.com/divergencetech/ethier/blob/main/contracts/random/NextShuffler.sol)
library LibPRNG {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The initial length must be greater than zero and less than `2**32 - 1`.
    error InvalidInitialLazyShufflerLength();

    /// @dev The new length must not be less than the current length.
    error InvalidNewLazyShufflerLength();

    /// @dev The lazy shuffler has not been initialized.
    error LazyShufflerNotInitialized();

    /// @dev Cannot double initialize the lazy shuffler.
    error LazyShufflerAlreadyInitialized();

    /// @dev The lazy shuffle has finished.
    error LazyShuffleFinished();

    /// @dev The queried index is out of bounds.
    error LazyShufflerGetOutOfBounds();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         CONSTANTS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The scalar of ETH and most ERC20s.
    uint256 internal constant WAD = 1e18;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STRUCTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev A pseudorandom number state in memory.
    struct PRNG {
        uint256 state;
    }

    /// @dev A lazy Fisher-Yates shuffler for a range `[0..n)` in storage.
    struct LazyShuffler {
        // Bits Layout:
        // - [0..31]    `numShuffled`
        // - [32..223]  `permutationSlot`
        // - [224..255] `length`
        uint256 _state;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         OPERATIONS                         */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Seeds the `prng` with `state`.
    function seed(PRNG memory prng, uint256 state) internal pure {
        /// @solidity memory-safe-assembly
        assembly {
            mstore(prng, state)
        }
    }

    /// @dev Returns the next pseudorandom uint256.
    /// All bits of the returned uint256 pass the NIST Statistical Test Suite.
    function next(PRNG memory prng) internal pure returns (uint256 result) {
        // We simply use `keccak256` for a great balance between
        // runtime gas costs, bytecode size, and statistical properties.
        //
        // A high-quality LCG with a 32-byte state
        // is only about 30% more gas efficient during runtime,
        // but requires a 32-byte multiplier, which can cause bytecode bloat
        // when this function is inlined.
        //
        // Using this method is about 2x more efficient than
        // `nextRandomness = uint256(keccak256(abi.encode(randomness)))`.
        /// @solidity memory-safe-assembly
        assembly {
            result := keccak256(prng, 0x20)
            mstore(prng, result)
        }
    }

    /// @dev Returns a pseudorandom uint256, uniformly distributed
    /// between 0 (inclusive) and `upper` (exclusive).
    /// If your modulus is big, this method is recommended
    /// for uniform sampling to avoid modulo bias.
    /// For uniform sampling across all uint256 values,
    /// or for small enough moduli such that the bias is neligible,
    /// use {next} instead.
    function uniform(PRNG memory prng, uint256 upper) internal pure returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            for {} 1 {} {
                result := keccak256(prng, 0x20)
                mstore(prng, result)
                if iszero(lt(result, mod(sub(0, upper), upper))) { break }
            }
            result := mod(result, upper)
        }
    }

    /// @dev Shuffles the array in-place with Fisher-Yates shuffle.
    function shuffle(PRNG memory prng, uint256[] memory a) internal pure {
        /// @solidity memory-safe-assembly
        assembly {
            let n := mload(a)
            let w := not(0)
            let mask := shr(128, w)
            if n {
                for { a := add(a, 0x20) } 1 {} {
                    // We can just directly use `keccak256`, cuz
                    // the other approaches don't save much.
                    let r := keccak256(prng, 0x20)
                    mstore(prng, r)

                    // Note that there will be a very tiny modulo bias
                    // if the length of the array is not a power of 2.
                    // For all practical purposes, it is negligible
                    // and will not be a fairness or security concern.
                    {
                        let j := add(a, shl(5, mod(shr(128, r), n)))
                        n := add(n, w) // `sub(n, 1)`.
                        if iszero(n) { break }

                        let i := add(a, shl(5, n))
                        let t := mload(i)
                        mstore(i, mload(j))
                        mstore(j, t)
                    }

                    {
                        let j := add(a, shl(5, mod(and(r, mask), n)))
                        n := add(n, w) // `sub(n, 1)`.
                        if iszero(n) { break }

                        let i := add(a, shl(5, n))
                        let t := mload(i)
                        mstore(i, mload(j))
                        mstore(j, t)
                    }
                }
            }
        }
    }

    /// @dev Shuffles the bytes in-place with Fisher-Yates shuffle.
    function shuffle(PRNG memory prng, bytes memory a) internal pure {
        /// @solidity memory-safe-assembly
        assembly {
            let n := mload(a)
            let w := not(0)
            let mask := shr(128, w)
            if n {
                let b := add(a, 0x01)
                for { a := add(a, 0x20) } 1 {} {
                    // We can just directly use `keccak256`, cuz
                    // the other approaches don't save much.
                    let r := keccak256(prng, 0x20)
                    mstore(prng, r)

                    // Note that there will be a very tiny modulo bias
                    // if the length of the array is not a power of 2.
                    // For all practical purposes, it is negligible
                    // and will not be a fairness or security concern.
                    {
                        let o := mod(shr(128, r), n)
                        n := add(n, w) // `sub(n, 1)`.
                        if iszero(n) { break }

                        let t := mload(add(b, n))
                        mstore8(add(a, n), mload(add(b, o)))
                        mstore8(add(a, o), t)
                    }

                    {
                        let o := mod(and(r, mask), n)
                        n := add(n, w) // `sub(n, 1)`.
                        if iszero(n) { break }

                        let t := mload(add(b, n))
                        mstore8(add(a, n), mload(add(b, o)))
                        mstore8(add(a, o), t)
                    }
                }
            }
        }
    }

    /// @dev Returns a sample from the standard normal distribution denominated in `WAD`.
    function standardNormalWad(PRNG memory prng) internal pure returns (int256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            // Technically, this is the Irwin-Hall distribution with 20 samples.
            // The chance of drawing a sample outside 10 σ from the standard normal distribution
            // is ≈ 0.000000000000000000000015, which is insignificant for most practical purposes.
            // Passes the Kolmogorov-Smirnov test for 200k samples. Uses about 322 gas.
            result := keccak256(prng, 0x20)
            mstore(prng, result)
            let n := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43 // Prime.
            let a := 0x100000000000000000000000000000051 // Prime and a primitive root of `n`.
            let m := 0x1fffffffffffffff1fffffffffffffff1fffffffffffffff1fffffffffffffff
            let s := 0x1000000000000000100000000000000010000000000000001
            let r1 := mulmod(result, a, n)
            let r2 := mulmod(r1, a, n)
            let r3 := mulmod(r2, a, n)
            // forgefmt: disable-next-item
            result := sub(sar(96, mul(26614938895861601847173011183,
                add(add(shr(192, mul(s, add(and(m, result), and(m, r1)))),
                shr(192, mul(s, add(and(m, r2), and(m, r3))))),
                shr(192, mul(s, and(m, mulmod(r3, a, n))))))), 7745966692414833770)
        }
    }

    /// @dev Returns a sample from the unit exponential distribution denominated in `WAD`.
    function exponentialWad(PRNG memory prng) internal pure returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            // Passes the Kolmogorov-Smirnov test for 200k samples.
            // Gas usage varies, starting from about 172+ gas.
            let r := keccak256(prng, 0x20)
            mstore(prng, r)
            let p := shl(129, r)
            let w := shl(1, r)
            if iszero(gt(w, p)) {
                let n := 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43 // Prime.
                let a := 0x100000000000000000000000000000051 // Prime and a primitive root of `n`.
                for {} 1 {} {
                    r := mulmod(r, a, n)
                    if iszero(lt(shl(129, r), w)) {
                        r := mulmod(r, a, n)
                        result := add(1000000000000000000, result)
                        w := shl(1, r)
                        p := shl(129, r)
                        if iszero(lt(w, p)) { break }
                        continue
                    }
                    w := shl(1, r)
                    if iszero(lt(w, shl(129, r))) { break }
                }
            }
            result := add(div(p, shl(129, 170141183460469231732)), result)
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*       STORAGE-BASED RANGE LAZY SHUFFLING OPERATIONS        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Initializes the state for lazy-shuffling the range `[0..n)`.
    /// Reverts if `n == 0 || n >= 2**32 - 1`.
    /// Reverts if `$` has already been initialized.
    /// If you need to reduce the length after initialization, just use a fresh new `$`.
    function initialize(LazyShuffler storage $, uint256 n) internal {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(lt(sub(n, 1), 0xfffffffe)) {
                mstore(0x00, 0x83b53941) // `InvalidInitialLazyShufflerLength()`.
                revert(0x1c, 0x04)
            }
            if sload($.slot) {
                mstore(0x00, 0x0c9f11f2) // `LazyShufflerAlreadyInitialized()`.
                revert(0x1c, 0x04)
            }
            mstore(0x00, $.slot)
            sstore($.slot, or(shl(224, n), shl(32, shr(64, keccak256(0x00, 0x20)))))
        }
    }

    /// @dev Increases the length of `$`.
    /// Reverts if `$` has not been initialized.
    function grow(LazyShuffler storage $, uint256 n) internal {
        /// @solidity memory-safe-assembly
        assembly {
            let state := sload($.slot) // The packed value at `$`.
            // If the new length is smaller than the old length, revert.
            if lt(n, shr(224, state)) {
                mstore(0x00, 0xbed37c6e) // `InvalidNewLazyShufflerLength()`.
                revert(0x1c, 0x04)
            }
            if iszero(state) {
                mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`.
                revert(0x1c, 0x04)
            }
            sstore($.slot, or(shl(224, n), shr(32, shl(32, state))))
        }
    }

    /// @dev Restarts the shuffler by setting `numShuffled` to zero,
    /// such that all elements can be drawn again.
    /// Restarting does NOT clear the internal permutation, nor changes the length.
    /// Even with the same sequence of randomness, reshuffling can yield different results.
    function restart(LazyShuffler storage $) internal {
        /// @solidity memory-safe-assembly
        assembly {
            let state := sload($.slot)
            if iszero(state) {
                mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`.
                revert(0x1c, 0x04)
            }
            sstore($.slot, shl(32, shr(32, state)))
        }
    }

    /// @dev Returns the number of elements that have been shuffled.
    function numShuffled(LazyShuffler storage $) internal view returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := and(0xffffffff, sload($.slot))
        }
    }

    /// @dev Returns the length of `$`.
    /// Returns zero if `$` is not initialized, else a non-zero value less than `2**32 - 1`.
    function length(LazyShuffler storage $) internal view returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := shr(224, sload($.slot))
        }
    }

    /// @dev Returns if `$` has been initialized.
    function initialized(LazyShuffler storage $) internal view returns (bool result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := iszero(iszero(sload($.slot)))
        }
    }

    /// @dev Returns if there are any more elements left to shuffle.
    /// Reverts if `$` is not initialized.
    function finished(LazyShuffler storage $) internal view returns (bool result) {
        /// @solidity memory-safe-assembly
        assembly {
            let state := sload($.slot) // The packed value at `$`.
            if iszero(state) {
                mstore(0x00, 0x1ead2566) // `LazyShufflerNotInitialized()`.
                revert(0x1c, 0x04)
            }
            result := eq(shr(224, state), and(0xffffffff, state))
        }
    }

    /// @dev Returns the current value stored at `index`, accounting for all historical shuffling.
    /// Reverts if `index` is greater than or equal to the `length` of `$`.
    function get(LazyShuffler storage $, uint256 index) internal view returns (uint256 result) {
        /// @solidity memory-safe-assembly
        assembly {
            let state := sload($.slot) // The packed value at `$`.
            let n := shr(224, state) // Length of `$`.
            if iszero(lt(index, n)) {
                mstore(0x00, 0x61367cc4) // `LazyShufflerGetOutOfBounds()`.
                revert(0x1c, 0x04)
            }
            let u32 := gt(n, 0xfffe)
            let s := add(shr(sub(4, u32), index), shr(64, shl(32, state))) // Bucket slot.
            let o := shl(add(4, u32), and(index, shr(u32, 15))) // Bucket slot offset (bits).
            let m := sub(shl(shl(u32, 16), 1), 1) // Value mask.
            result := and(m, shr(o, sload(s)))
            result := xor(index, mul(xor(index, sub(result, 1)), iszero(iszero(result))))
        }
    }

    /// @dev Does a single Fisher-Yates shuffle step, increments the `numShuffled` in `$`,
    /// and returns the next value in the shuffled range.
    /// `randomness` can be taken from a good-enough source, or a higher quality source like VRF.
    /// Reverts if there are no more values to shuffle, which includes the case if `$` is not initialized.
    function next(LazyShuffler storage $, uint256 randomness) internal returns (uint256 chosen) {
        /// @solidity memory-safe-assembly
        assembly {
            function _get(u32_, state_, i_) -> _value {
                let s_ := add(shr(sub(4, u32_), i_), shr(64, shl(32, state_))) // Bucket slot.
                let o_ := shl(add(4, u32_), and(i_, shr(u32_, 15))) // Bucket slot offset (bits).
                let m_ := sub(shl(shl(u32_, 16), 1), 1) // Value mask.
                _value := and(m_, shr(o_, sload(s_)))
                _value := xor(i_, mul(xor(i_, sub(_value, 1)), iszero(iszero(_value))))
            }
            function _set(u32_, state_, i_, value_) {
                let s_ := add(shr(sub(4, u32_), i_), shr(64, shl(32, state_))) // Bucket slot.
                let o_ := shl(add(4, u32_), and(i_, shr(u32_, 15))) // Bucket slot offset (bits).
                let m_ := sub(shl(shl(u32_, 16), 1), 1) // Value mask.
                let v_ := sload(s_) // Bucket slot value.
                value_ := mul(iszero(eq(i_, value_)), add(value_, 1))
                sstore(s_, xor(v_, shl(o_, and(m_, xor(shr(o_, v_), value_)))))
            }
            let state := sload($.slot) // The packed value at `$`.
            let shuffled := and(0xffffffff, state) // Number of elements shuffled.
            let n := shr(224, state) // Length of `$`.
            let remainder := sub(n, shuffled) // Number of elements left to shuffle.
            if iszero(remainder) {
                mstore(0x00, 0x51065f79) // `LazyShuffleFinished()`.
                revert(0x1c, 0x04)
            }
            mstore(0x00, randomness) // (Re)hash the randomness so that we don't
            mstore(0x20, shuffled) // need to expect guarantees on its distribution.
            let index := add(mod(keccak256(0x00, 0x40), remainder), shuffled)
            chosen := _get(gt(n, 0xfffe), state, index)
            _set(gt(n, 0xfffe), state, index, _get(gt(n, 0xfffe), state, shuffled))
            _set(gt(n, 0xfffe), state, shuffled, chosen)
            sstore($.slot, add(1, state)) // Increment the `numShuffled` by 1, and store it.
        }
    }
}

// SPDX-License-Identifier: BUSL-1.1
/**
            ▒▓▒  ▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓███▓▓▒     ▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓██▓
             ▒██▒▓▓▓▓█▓██████████████████▓  ▒▒▒▓███████████████▒
              ▒██▒▓█████████████████████▒ ▒▓██████████▓███████
               ▒███████████▓▒                   ▒███▓▓██████▓
                 █████████▒                     ▒▓▒▓███████▒
                  ███████▓      ▒▒▒▒▒▓▓█▓▒     ▓█▓████████
                   ▒▒▒▒▒   ▒▒▒▒▓▓▓█████▒      ▓█████████▓
                         ▒▓▓▓▒▓██████▓      ▒▓▓████████▒
                       ▒██▓▓▓███████▒      ▒▒▓███▓████
                        ▒███▓█████▒       ▒▒█████▓██▓
                          ██████▓   ▒▒▒▓██▓██▓█████▒
                           ▒▒▓▓▒   ▒██▓▒▓▓████████
                                  ▓█████▓███████▓
                                 ██▓▓██████████▒
                                ▒█████████████
                                 ███████████▓
      ▒▓▓▓▓▓▓▒▓                  ▒█████████▒                      ▒▓▓
    ▒▓█▒   ▒▒█▒▒                   ▓██████                       ▒▒▓▓▒
   ▒▒█▒       ▓▒                    ▒████                       ▒▓█▓█▓▒
   ▓▒██▓▒                             ██                       ▒▓█▓▓▓██▒
    ▓█▓▓▓▓▓█▓▓▓▒        ▒▒▒         ▒▒▒▓▓▓▓▒▓▒▒▓▒▓▓▓▓▓▓▓▓▒    ▒▓█▒ ▒▓▒▓█▓
     ▒▓█▓▓▓▓▓▓▓▓▓▓▒    ▒▒▒▓▒     ▒▒▒▓▓     ▓▓  ▓▓█▓   ▒▒▓▓   ▒▒█▒   ▒▓▒▓█▓
            ▒▒▓▓▓▒▓▒  ▒▓▓▓▒█▒   ▒▒▒█▒          ▒▒█▓▒▒▒▓▓▓▒   ▓██▓▓▓▓▓▓▓███▓
 ▒            ▒▓▓█▓  ▒▓▓▓▓█▓█▓  ▒█▓▓▒          ▓▓█▓▒▓█▓▒▒   ▓█▓        ▓███▓
▓▓▒         ▒▒▓▓█▓▒▒▓█▒   ▒▓██▓  ▓██▓▒     ▒█▓ ▓▓██   ▒▓▓▓▒▒▓█▓        ▒▓████▒
 ██▓▓▒▒▒▒▓▓███▓▒ ▒▓▓▓▓▒▒ ▒▓▓▓▓▓▓▓▒▒▒▓█▓▓▓▓█▓▓▒▒▓▓▓▓▓▒    ▒▓████▓▒     ▓▓███████▓▓▒
*/
pragma solidity 0.8.23;

import "../interfaces/IAppErrors.sol";
import "../interfaces/IERC20.sol";
import "../interfaces/IERC721.sol";
import "../interfaces/IGuildBank.sol";
import "../interfaces/IGuildController.sol";
import {IApplicationEvents} from "../interfaces/IApplicationEvents.sol";

contract GuildBank is IGuildBank {

  //region ------------------------ CONSTANTS
  /// @notice Version of the contract
  /// @dev Should be incremented when contract changed
  string public constant VERSION = "1.0.1";
  //endregion ------------------------ CONSTANTS

  //region ------------------------ Members
  IGuildController immutable public guildController;
  uint immutable public guildId;
  //endregion ------------------------ Members

  //region ------------------------ Restrictions and constructor
  function _onlyGuildController(address msgSender) internal view {
    if (msgSender != address(guildController)) revert IAppErrors.GuildControllerOnly();
  }

  constructor (address guildController_, uint guildId_) {
    guildController = IGuildController(guildController_);
    guildId = guildId_;
  }
  //endregion ------------------------ Restrictions and constructor

  //region ------------------------ ERC20
  function transfer(address token, address recipient, uint amount) external {
    _onlyGuildController(msg.sender);

    IERC20(token).transfer(recipient, amount);
    emit IApplicationEvents.GuildBankTransfer(token, recipient, amount);
  }

  function approve(address token, address spender, uint256 amount) external returns (bool) {
    _onlyGuildController(msg.sender);

    return IERC20(token).approve(spender, amount);
  }
  //endregion ------------------------ ERC20

  //region ------------------------ ERC721
  function transferNft(address to, address nft, uint256 tokenId) external {
    _onlyGuildController(msg.sender);

    IERC721(nft).transferFrom(address(this), to, tokenId);
    emit IApplicationEvents.GuildBankTransferNft(to, nft, tokenId);
  }

  function transferNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external {
    _onlyGuildController(msg.sender);

    uint len = nfts.length;
    if (len != tokenIds.length) revert IAppErrors.LengthsMismatch();

    for (uint i; i < len; ++i) {
      IERC721(nfts[i]).transferFrom(address(this), to, tokenIds[i]);
    }
    emit IApplicationEvents.GuildBankTransferNftMulti(to, nfts, tokenIds);
  }

  function approveNft(address to, address nft, uint256 tokenId) external {
    _onlyGuildController(msg.sender);

    IERC721(nft).approve(to, tokenId);
  }

  function approveNftMulti(address to, address[] memory nfts, uint256[] memory tokenIds) external {
    _onlyGuildController(msg.sender);

    uint len = nfts.length;
    if (len != tokenIds.length) revert IAppErrors.LengthsMismatch();

    for (uint i; i < len; ++i) {
      IERC721(nfts[i]).approve(to, tokenIds[i]);
    }
  }
  //endregion ------------------------ ERC721
}

Context size (optional):