diff --git a/cmake/modules/CanaryLib.cmake b/cmake/modules/CanaryLib.cmake index 916970c37ea..58ae1e31f06 100644 --- a/cmake/modules/CanaryLib.cmake +++ b/cmake/modules/CanaryLib.cmake @@ -138,4 +138,4 @@ if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" OR CMAKE_BUILD_TYPE STREQUAL "Rele elseif(MSVC) target_compile_options(${PROJECT_NAME}_lib PRIVATE /O2) endif() -endif() +endif() \ No newline at end of file diff --git a/config.lua.dist b/config.lua.dist index 7d0360d9360..fbffa27db9d 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -105,6 +105,7 @@ augmentStrongImpactPercent = 10 -- NOTE: preyFreeRerollTime: Time in seconds that players will have to wait to get a new free prey list. preySystemEnabled = true preyFreeThirdSlot = false +preyMaxCardsAmount = 50 preyRerollPricePerLevel = 200 preySelectListPrice = 5 preyBonusRerollPrice = 1 @@ -282,6 +283,7 @@ startStreakLevel = 0 showLootsInBestiary = false minTownIdToBankTransferFromMain = 4 enableSupportOutfit = true +instantDailyRewardAccessAmount = 90 -- Teleport summon -- Set to true will never remove the summon diff --git a/data-otservbr-global/migrations/48.lua b/data-otservbr-global/migrations/48.lua new file mode 100644 index 00000000000..0db69e9c951 --- /dev/null +++ b/data-otservbr-global/migrations/48.lua @@ -0,0 +1,12 @@ +function onUpdateDatabase() + logger.info("Updating database to version 48 (migrate gamestore to cpp)") + + db.query([[ + ALTER TABLE `store_history` + ADD `type` smallint(2) UNSIGNED NOT NULL DEFAULT '0', + ADD `show_detail` smallint(2) UNSIGNED NOT NULL DEFAULT '0', + CHANGE `timestamp` `player_name` varchar(255) DEFAULT NULL, + CHANGE `coins` `total_price` bigint NOT NULL DEFAULT '0', + CHANGE `time` `created_at` bigint UNSIGNED NOT NULL DEFAULT '0' + ]]) +end diff --git a/data/XML/store/readme.md b/data/XML/store/readme.md new file mode 100644 index 00000000000..475db6072f9 --- /dev/null +++ b/data/XML/store/readme.md @@ -0,0 +1,109 @@ +## Store + +--- + +### FAQ + +##### 1. Store XML Structure + +```xml + + + + + + + + + + + + + +``` + +### Category Options + +| Field Name | Type | Usage | Default | +| ---------- | --------------- | --------------------------- | --------- | +| name | string | category name | Mandatory | +| icon | string | category icon name | Mandatory | +| rookgaard | string (yes/no) | category is allowed in rook | yes | + +#### Example : + +```xml + +``` + +### Subcategory Options + +| Field Name | Type | Usage | Default | +| ---------- | --------------- | ------------------------------ | --------- | +| name | string | subcategory name | Mandatory | +| icon | string | subcategory icon name | Mandatory | +| rookgaard | string (yes/no) | subcategory is allowed in rook | yes | +| state | string | subcategory highlight state | none | + +### State types + +| State Options | +| ------------- | +| none | +| new | +| sale | +| timed | + +#### Example : + +```xml + + +``` + +### Offer Options + +#### Mandatory Fields + +| Method | Type | Usage | +| ------- | ------------ | ------------------------------------ | +| name | string | offer name | +| icon | string | offer icon name | +| offerId | unsigned int | offer identifier | +| price | unsigned int | offer price | +| type | string | offer type (possible options below) | +| state | string | offer state (possible options below) | + +### Offer types + +| | | Type Options | | | +| --------- | ---------- | ------------ | ------------- | -------------- | +| none | mount | preyslot | pounch | namechange | +| item | namechange | preybonus | allblessings | sexchange | +| stackable | sexchange | temple | instantreward | hirelingskill | +| charges | house | bleesings | charms | hirelingoutfit | +| outfit | expboost | premium | hireling | huntingslot | + +#### Optional Fields + +| Method | Type | Usage | Default | +| ----------- | ------------ | ----------------- | ------- | +| count | unsigned int | offer count | 1 | +| validUntil | unsigned int | offer validUntil | 0 | +| coinType | string | offer coin type | coin | +| description | string | offer description | "" | +| movable | string | offer movable | false | + +### Coin types + +| Coin Options | +| ------------ | +| coin | +| transferable | +| tournament | + +#### Example : + +```XML + +``` diff --git a/data/XML/store/store.xml b/data/XML/store/store.xml new file mode 100644 index 00000000000..4e7df7c1a6f --- /dev/null +++ b/data/XML/store/store.xml @@ -0,0 +1,800 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/modules/modules.xml b/data/modules/modules.xml index e51bf055f2a..1e1c2c62531 100644 --- a/data/modules/modules.xml +++ b/data/modules/modules.xml @@ -1,15 +1,5 @@ - - - - - - - - - - diff --git a/data/modules/scripts/gamestore/gamestore.lua b/data/modules/scripts/gamestore/gamestore.lua deleted file mode 100644 index ed17c456c0d..00000000000 --- a/data/modules/scripts/gamestore/gamestore.lua +++ /dev/null @@ -1,6777 +0,0 @@ ---[[ -Items have been updated so that if the offer type is not one of the types: OFFER_TYPE_OUTFIT, OFFER_TYPE_OUTFIT_ADDON, -OFFER_TYPE_MOUNT, OFFER_TYPE_NAMECHANGE, OFFER_TYPE_SEXCHANGE, OFFER_TYPE_PROMOTION, OFFER_TYPE_EXPBOOST, -OFFER_TYPE_PREYSLOT, OFFER_TYPE_PREYBONUS, OFFER_TYPE_TEMPLE, OFFER_TYPE_BLESSINGS, OFFER_TYPE_PREMIUM, -OFFER_TYPE_ALLBLESSINGS -]] - --- Parser -dofile(CORE_DIRECTORY .. "/modules/scripts/gamestore/init.lua") --- Config - -HomeBanners = { - images = { "home/banner_armouredarcher.png", "home/banner_podiumoftenacity.png" }, - delay = 10, -} - -local premiumCategoryName = "Premium Time" -local premiumOfferName = "Premium Time" -if configManager.getBoolean(configKeys.VIP_SYSTEM_ENABLED) then - premiumCategoryName = "VIP Shop" - premiumOfferName = "VIP" -end - -local premiumDescription = - "Enhance your gaming experience by gaining additional abilities and advantages:\n\n• access to Premium areas\n• use Tibia's transport system (ships, carpet)\n• more spells\n• rent houses\n• found guilds\n• offline training\n• larger depots\n• and many more\n\n{usablebyallicon} valid for all characters on this account\n{activated}" -if configManager.getBoolean(configKeys.VIP_SYSTEM_ENABLED) then - local vipBonusExp = configManager.getNumber(configKeys.VIP_BONUS_EXP) - local vipBonusLoot = configManager.getNumber(configKeys.VIP_BONUS_LOOT) - local vipBonusSkill = configManager.getNumber(configKeys.VIP_BONUS_SKILL) - local vipStayOnline = configManager.getBoolean(configKeys.VIP_STAY_ONLINE) - - premiumDescription = "Enhance your gaming experience by gaining advantages:\n\n" - if vipBonusExp > 0 then - premiumDescription = premiumDescription .. "• +" .. vipBonusExp .. "% experience rate\n" - end - if vipBonusSkill > 0 then - premiumDescription = premiumDescription .. "• +" .. vipBonusSkill .. "% skill training speed\n" - end - if vipBonusLoot > 0 then - premiumDescription = premiumDescription .. "• +" .. vipBonusLoot .. "% loot\n" - end - if vipStayOnline then - premiumDescription = premiumDescription .. "• stay online idle without getting disconnected\n" - end - premiumDescription = premiumDescription .. "\n{usablebyallicon} valid for all characters on this account\n{activated}" -end - --- GameStore.SearchCategory = { --- icons = {}, --- name = "Search Results", --- rookgaard = true, --- state = GameStore.States.STATE_NONE, --- } - -GameStore.Categories = { - -- Premium Time - { - icons = { "Category_PremiumTime.png" }, - name = premiumCategoryName, - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Premium_Time_30.png" }, - name = string.format("30 Days of %s", premiumOfferName), - price = 250, - id = 3030, - validUntil = 30, - description = premiumDescription, - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_90.png" }, - name = string.format("90 Days of %s", premiumOfferName), - price = 750, - id = 3090, - validUntil = 90, - description = premiumDescription, - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_180.png" }, - name = string.format("180 Days of %s", premiumOfferName), - price = 1500, - id = 3180, - validUntil = 180, - description = premiumDescription, - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - { - icons = { "Premium_Time_360.png" }, - name = string.format("360 Days of %s", premiumOfferName), - price = 3000, - id = 3360, - validUntil = 360, - description = premiumDescription, - type = GameStore.OfferTypes.OFFER_TYPE_PREMIUM, - }, - }, - }, - -- Consumables - { - icons = { "Category_Consumables.png" }, - name = "Consumables", - rookgaard = true, - subclasses = { "Blessings", "Casks", "Exercise Weapons", "Kegs", "Potions", "Runes" }, - }, - -- Consumables ~ Blessings - { - icons = { "Category_Blessings.png" }, - name = "Blessings", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "All_PvE_Blessings.png" }, - name = "All Regular Blessings", - price = 130, - id = GameStore.SubActions.BLESSING_ALL_PVE, - count = 1, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, - }, - { - icons = { "All_PvE_Blessings.png" }, - name = "All Regular Blessings", - price = 650, - id = GameStore.SubActions.BLESSING_ALL_PVP, - count = 5, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, - }, - { - icons = { "Blood_of_the_Mountain.png" }, - name = "Blood of the Mountain", - price = 25, - blessid = 7, - count = 1, - id = GameStore.SubActions.BLESSING_BLOOD, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Death_Redemption.png" }, - name = "Death Redemption", - price = 260, - blessid = 10, - count = 1, - description = "Reduces the penalty of your character's most recent death.\n\n{character}\n{info} can only be used for the most recent death and only within 24 hours after this death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Heart_of_the_Mountain.png" }, - name = "Heart of the Mountain", - price = 25, - blessid = 8, - count = 1, - id = GameStore.SubActions.BLESSING_HEART, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Embrace_of_Tibia.png" }, - name = "The Embrace of Tibia", - price = 15, - blessid = 6, - count = 1, - id = GameStore.SubActions.BLESSING_EMBRACE, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Fire_of_the_Suns.png" }, - name = "The Fire of the Suns", - price = 15, - blessid = 4, - count = 1, - id = GameStore.SubActions.BLESSING_SUNS, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Spark_of_the_Phoenix.png" }, - name = "The Spark of the Phoenix", - price = 20, - blessid = 3, - count = 1, - id = GameStore.SubActions.BLESSING_PHOENIX, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Spiritual_Shielding.png" }, - name = "The Spiritual Shielding", - price = 15, - blessid = 5, - count = 1, - id = GameStore.SubActions.BLESSING_SPIRITUAL, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Wisdom_of_Solitude.png" }, - name = "The Wisdom of Solitude", - price = 15, - blessid = 2, - count = 1, - id = GameStore.SubActions.BLESSING_SOLITUDE, - description = "Reduces your character's chance to lose any items as well as the amount of your character's experience and skill loss upon death:\n\n• 1 blessing = 8.00% less Skill / XP loss, 30% equipment protection\n• 2 blessing = 16.00% less Skill / XP loss, 55% equipment protection\n• 3 blessing = 24.00% less Skill / XP loss, 75% equipment protection\n• 4 blessing = 32.00% less Skill / XP loss, 90% equipment protection\n• 5 blessing = 40.00% less Skill / XP loss, 100% equipment protection\n• 6 blessing = 48.00% less Skill / XP loss, 100% equipment protection\n• 7 blessing = 56.00% less Skill / XP loss, 100% equipment protection\n\n{character} \n{limit|5} \n{info} added directly to the Record of Blessings \n{info} characters with a red or black skull will always lose all equipment upon death", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Twist_of_Fate.png" }, - name = "Twist of Fate", - price = 8, - blessid = 1, - count = 1, - id = GameStore.SubActions.BLESSING_TWIST, - description = "Protects your character's regular blessings or an Amulet of Loss if you are unfortunate enough to die in a PvP fight.\n\n{character}\n{limit|5}\n{info} added directly to the Record of Blessings\n{info} does not work for characters with a red or black skull", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - { - icons = { "Twist_of_Fate.png" }, - name = "Twist of Fate", - price = 40, - blessid = 1, - count = 5, - description = "Protects your character's regular blessings or an Amulet of Loss if you are unfortunate enough to die in a PvP fight.\n\n{character}\n{limit|5}\n{info} added directly to the Record of Blessings\n{info} does not work for characters with a red or black skull", - type = GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, - }, - }, - }, - -- Consumables ~ Casks - { - icons = { "Category_Casks.png" }, - name = "Casks", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Great_Health_Cask.png" }, - name = "Great Health Cask", - price = 22, - itemtype = 25881, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Great_Mana_Cask.png" }, - name = "Great Mana Cask", - price = 14, - itemtype = 25891, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Great_Spirit_Cask.png" }, - name = "Great Spirit Cask", - price = 22, - itemtype = 25899, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points and mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Health_Cask.png" }, - name = "Health Cask", - price = 5, - itemtype = 25879, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mana_Cask.png" }, - name = "Mana Cask", - price = 5, - itemtype = 25889, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Strong_Health_Cask.png" }, - name = "Strong Health Cask", - price = 11, - itemtype = 25880, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Strong_Mana_Cask.png" }, - name = "Strong Mana Cask", - price = 9, - itemtype = 25890, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Supreme_Health_Cask.png" }, - name = "Supreme Health Cask", - price = 59, - itemtype = 25883, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Health_Cask.png" }, - name = "Ultimate Health Cask", - price = 36, - itemtype = 25882, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Mana_Cask.png" }, - name = "Ultimate Mana Cask", - price = 42, - itemtype = 25892, - count = 1000, - description = "Place it in your house and fill up potions to refill your mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ultimate_Spirit_Cask.png" }, - name = "Ultimate Spirit Cask", - price = 42, - itemtype = 25900, - count = 1000, - description = "Place it in your house and fill up potions to restore your hit points and mana!\n\n{house}\n{box}\n{storeinbox}\n{usablebyallicon} can be used to fill up potions by all characters that have access to the house\n{storeinboxicon} potions created from this cask will be sent to your Store inbox and can only be stored there and in depot box\n{backtoinbox}\n{info} usable 1000 times a piece\n{transferableprice}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- Consumables ~ Exercise Weapons - { - icons = { "Category_ExerciseWeapons.png" }, - name = "Exercise Weapons", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Durable_Exercise_Axe.png" }, - name = "Durable Exercise Axe", - price = 90, - itemtype = 35280, - charges = 1800, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Bow.png" }, - name = "Durable Exercise Bow", - price = 90, - itemtype = 35282, - charges = 1800, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Club.png" }, - name = "Durable Exercise Club", - price = 90, - itemtype = 35281, - charges = 1800, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Rod.png" }, - name = "Durable Exercise Rod", - price = 90, - itemtype = 35283, - charges = 1800, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Sword.png" }, - name = "Durable Exercise Sword", - price = 90, - itemtype = 35279, - charges = 1800, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Wand.png" }, - name = "Durable Exercise Wand", - price = 90, - itemtype = 35284, - charges = 1800, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Durable_Exercise_Shield.png" }, - name = "Durable Exercise Shield", - price = 90, - itemtype = 44066, - charges = 1800, - description = "Use it to train your shielding skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your shielding skill\n{info} usable 1800 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Axe.png" }, - name = "Exercise Axe", - price = 25, - itemtype = 28553, - charges = 500, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Bow.png" }, - name = "Exercise Bow", - price = 25, - itemtype = 28555, - charges = 500, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Club.png" }, - name = "Exercise Club", - price = 25, - itemtype = 28554, - charges = 500, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Rod.png" }, - name = "Exercise Rod", - price = 25, - itemtype = 28556, - charges = 500, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Sword.png" }, - name = "Exercise Sword", - price = 25, - itemtype = 28552, - charges = 500, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Wand.png" }, - name = "Exercise Wand", - price = 25, - itemtype = 28557, - charges = 500, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Exercise_Shield.png" }, - name = "Exercise Shield", - price = 25, - itemtype = 44065, - charges = 500, - description = "Use it to train your shielding skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your shielding skill\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Axe.png" }, - name = "Lasting Exercise Axe", - price = 720, - itemtype = 35286, - charges = 14400, - description = "Use it to train your axe fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your axe fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Bow.png" }, - name = "Lasting Exercise Bow", - price = 720, - itemtype = 35288, - charges = 14400, - description = "Use it to train your distance fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your distance fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Club.png" }, - name = "Lasting Exercise Club", - price = 720, - itemtype = 35287, - charges = 14400, - description = "Use it to train your club fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your club fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Rod.png" }, - name = "Lasting Exercise Rod", - price = 720, - itemtype = 35289, - charges = 14400, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Sword.png" }, - name = "Lasting Exercise Sword", - price = 720, - itemtype = 35285, - charges = 14400, - description = "Use it to train your sword fighting skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your sword fighting skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Wand.png" }, - name = "Lasting Exercise Wand", - price = 720, - itemtype = 35290, - charges = 14400, - description = "Use it to train your magic level on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your magic level\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Lasting_Exercise_Shield.png" }, - name = "Lasting Exercise Shield", - price = 720, - itemtype = 44067, - charges = 14400, - description = "Use it to train your shielding skill on an exercise dummy!\n\n{character}\n{storeinbox}\n{info} use it on an exercise dummy to train your shielding skill\n{info} usable 14400 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - }, - }, - -- Consumables ~ Kegs - { - icons = { "Category_Kegs.png" }, - name = "Kegs", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Great_Health_Keg.png" }, - name = "Great Health Keg", - price = 103, - itemtype = 25905, - charges = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Great_Mana_Keg.png" }, - name = "Great Mana Keg", - price = 66, - itemtype = 25910, - charges = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Great_Spirit_Keg.png" }, - name = "Great Spirit Keg", - price = 105, - itemtype = 25913, - charges = 500, - description = "Fill up potions to restore your hit points and mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Health_Keg.png" }, - name = "Health Keg", - price = 26, - itemtype = 25903, - charges = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Mana_Keg.png" }, - name = "Mana Keg", - price = 26, - itemtype = 25908, - charges = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Strong_Health_Keg.png" }, - name = "Strong Health Keg", - price = 53, - itemtype = 25904, - charges = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Strong_Mana_Keg.png" }, - name = "Strong Mana Keg", - price = 43, - itemtype = 25909, - charges = 500, - description = "Fill up potions to refill your mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Supreme_Health_Keg.png" }, - name = "Supreme Health Keg", - price = 288, - itemtype = 25907, - charges = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Ultimate_Health_Keg.png" }, - name = "Ultimate Health Keg", - price = 175, - itemtype = 25906, - count = 500, - description = "Fill up potions to restore your hit points no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Ultimate_Mana_Keg.png" }, - name = "Ultimate Mana Keg", - price = 202, - itemtype = 25911, - charges = 500, - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Ultimate_Spirit_Keg.png" }, - name = "Ultimate Spirit Keg", - price = 202, - itemtype = 25914, - charges = 500, - description = "Fill up potions to restore your hit points and mana no matter where you are!\n\n{character}\n{vocationlevelcheck}\n{storeinboxicon} potions created from this keg will be sent to your Store inbox and can only be stored there and in depot box\n{info} usable 500 times a piece\n{info} saves capacity because it's constant weight equals only 250 potions", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - }, - }, - -- Consumables ~ Potions - { - icons = { "Category_Potions.png" }, - name = "Potions", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Great_Health_Potion.png" }, - name = "Great Health Potion", - price = 18, - itemtype = 239, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Health_Potion.png" }, - name = "Great Health Potion", - price = 41, - itemtype = 239, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Mana_Potion.png" }, - name = "Great Mana Potion", - price = 11, - itemtype = 238, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Mana_Potion.png" }, - name = "Great Mana Potion", - price = 26, - itemtype = 238, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n-{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Spirit_Potion.png" }, - name = "Great Spirit Potion", - price = 18, - itemtype = 7642, - count = 100, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Spirit_Potion.png" }, - name = "Great Spirit Potion", - price = 41, - itemtype = 7642, - count = 250, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Health_Potion.png" }, - name = "Health Potion", - price = 6, - itemtype = 266, - count = 125, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Health_Potion.png" }, - name = "Health Potion", - price = 11, - itemtype = 266, - count = 300, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Mana_Potion.png" }, - name = "Mana Potion", - price = 6, - itemtype = 268, - count = 125, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Mana_Potion.png" }, - name = "Mana Potion", - price = 12, - itemtype = 268, - count = 300, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Health_Potion.png" }, - name = "Strong Health Potion", - price = 10, - itemtype = 236, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Health_Potion.png" }, - name = "Strong Health Potion", - price = 21, - itemtype = 236, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Mana_Potion.png" }, - name = "Strong Mana Potion", - price = 7, - itemtype = 237, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Strong_Mana_Potion.png" }, - name = "Strong Mana Potion", - price = 17, - itemtype = 237, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Supreme_Health_Potion.png" }, - name = "Supreme Health Potion", - price = 47, - itemtype = 23375, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Supreme_Health_Potion.png" }, - name = "Supreme Health Potion", - price = 113, - itemtype = 23375, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Health_Potion.png" }, - name = "Ultimate Health Potion", - price = 29, - itemtype = 7643, - count = 100, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Health_Potion.png" }, - name = "Ultimate Health Potion", - price = 68, - itemtype = 7643, - count = 250, - description = "Restores your character's hit points.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Mana_Potion.png" }, - name = "Ultimate Mana Potion", - price = 33, - itemtype = 23373, - count = 100, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Mana_Potion.png" }, - name = "Ultimate Mana Potion", - price = 79, - itemtype = 23373, - count = 250, - description = "Refills your character's mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Spirit_Potion.png" }, - name = "Ultimate Spirit Potion", - price = 33, - itemtype = 23374, - count = 100, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Spirit_Potion.png" }, - name = "Ultimate Spirit Potion", - price = 79, - itemtype = 23374, - count = 250, - description = "Restores your character's hit points and mana.\n\n{character}\n{vocationlevelcheck}\n{storeinbox}\n{battlesign}\n{capacity}", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - }, - }, - -- Consumables ~ Runes - { - icons = { "Category_Runes.png" }, - name = "Runes", - parent = "Consumables", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Animate_Dead_Rune.png" }, - name = "Animate Dead Rune", - price = 75, - itemtype = 3203, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck} only buyable if fitting vocation and level of purchasing character\n{battlesign}\n{capacity}\n\nAfter a long time of research, the magicians of Edron succeeded in storing some life energy in a rune. When this energy was unleashed onto a body it was found that an undead creature arose that could be mentally controlled by the user of the rune. This rune is useful to create allies in combat.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Avalanche_Rune.png" }, - name = "Avalanche Rune", - price = 12, - itemtype = 3161, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe ice damage which arises from this rune is a useful weapon in every battle but it comes in particularly handy if you fight against a horde of creatures dominated by the element fire.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Chameleon_Rune.png" }, - name = "Chameleon Rune", - price = 42, - itemtype = 3178, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe metamorphosis caused by this rune is only superficial, and while casters who are using the rune can take on the exterior form of nearly any inanimate object, they will always retain their original smell and mental abilities. So there is no real practical use for this rune, making this largely a fun rune.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Convince_Creature_Rune.png" }, - name = "Convince Creature Rune", - price = 16, - itemtype = 3177, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nUsing this rune together with some mana, you can convince certain creatures. The needed amount of mana is determined by the power of the creature one wishes to convince, so the amount of mana to convince a rat is lower than that which is needed for an orc.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Cure_Poison_Rune_(Item).png" }, - name = "Cure Poison Rune", - price = 13, - itemtype = 3153, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nIn the old days, many adventurers fell prey to poisonous creatures that were roaming the caves and forests. After many years of research druids finally succeeded in altering the cure poison spell so it could be bound to a rune. By using this rune it is possible to stop the effect of any known poison.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Disintegrate_Rune.png" }, - name = "Disintegrate Rune", - price = 5, - itemtype = 3197, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nNothing is worse than being cornered when fleeing from an enemy you just cannot beat, especially if the obstacles in your way are items you could easily remove if only you had the time! However, there is one reliable remedy: The Disintegrate rune will instantly destroy up to 500 movable items that are in your way, making room for a quick escape.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Bomb_Rune.png" }, - name = "Energy Bomb Rune", - price = 40, - itemtype = 3149, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nUsing the Energy Bomb rune will create a field of deadly energy that deals damage to all who carelessly step into it. Its area of effect is covering a full 9 square metres! Creatures that are caught in the middle of an Energy Bomb are frequently confused by the unexpected effect, and some may even stay in the field of deadly sparks for a while.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Field_Rune.png" }, - name = "Energy Field Rune", - price = 8, - itemtype = 3164, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis spell creates a limited barrier made up of crackling energy that will cause electrical damage to all those passing through. Since there are few creatures that are immune to the harmful effects of energy this spell is not to be underestimated.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Energy_Wall_Rune.png" }, - name = "Energy Wall Rune", - price = 17, - itemtype = 3166, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nCasting this spell generates a solid wall made up of magical energy. Walls made this way surpass any other magically created obstacle in width, so it is always a good idea to have an Energy Wall rune or two in one's pocket when travelling through the wilderness.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Explosion_Rune.png" }, - name = "Explosion Rune", - price = 6, - itemtype = 3200, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune must be aimed at areas rather than at specific creatures, so it is possible for explosions to be unleashed even if no targets are close at all. These explosions cause a considerable physical damage within a substantial blast radius.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fireball_Rune.png" }, - name = "Fireball Rune", - price = 6, - itemtype = 3189, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a massive fiery ball is released which hits the aimed foe with immense power. It is especially effective against opponents of the element earth.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Bomb_Rune.png" }, - name = "Fire Bomb Rune", - price = 29, - itemtype = 3192, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune is a deadly weapon in the hands of the skilled user. On releasing it an area of 9 square metres is covered by searing flames that will scorch all those that are unfortunate enough to be caught in them. Worse, many monsters are confused by the unexpected blaze, and with a bit of luck a caster will even manage to trap his opponents by using the spell.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Field_Rune.png" }, - name = "Fire Field Rune", - price = 6, - itemtype = 3188, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a field of one square metre is covered by searing fire that will last for some minutes, gradually diminishing as the blaze wears down. As with all field spells, Fire Field is quite useful to block narrow passageways or to create large, connected barriers.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Fire_Wall_Rune.png" }, - name = "Fire Wall Rune", - price = 12, - itemtype = 3190, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune offers reliable protection against all creatures that are afraid of fire. The exceptionally long duration of the spell as well as the possibility to form massive barriers or even protective circles out of fire walls make this a versatile, practical spell.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Great_Fireball_Rune.png" }, - name = "Great Fireball Rune", - price = 12, - itemtype = 3191, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nA shot of this rune affects a huge area - up to 37 square metres! It stands to reason that the Great Fireball is a favourite of most Tibians, as it is well suited both to hit whole crowds of monsters and individual targets that are difficult to hit because they are fast or hard to spot.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Icicle_Rune.png" }, - name = "Icicle Rune", - price = 6, - itemtype = 3158, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nParticularly creatures determined by the element fire are vulnerable against this ice-cold rune. Being hit by the magic stored in this rune, an ice arrow seems to pierce the heart of the struck victim. The damage done by this rune is quite impressive which makes this a quite popular rune among Tibian mages.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Intense_Healing_Rune.png" }, - name = "Intense Healing Rune", - price = 19, - itemtype = 3152, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune is commonly used by young adventurers who are not skilled enough to use the rune's stronger version. Also, since the rune's effectiveness is determined by the user's magic skill, it is still popular among experienced spell casters who use it to get effective healing magic at a cheap price.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Magic_Wall_Rune.png" }, - name = "Magic Wall Rune", - price = 23, - itemtype = 3180, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis spell causes all particles that are contained in the surrounding air to quickly gather and contract until a solid wall is formed that covers one full square metre. The wall that is formed that way is impenetrable to any missiles or to light and no creature or character can walk through it. However, the wall will only last for a couple of seconds.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Poison_Bomb_Rune.png" }, - name = "Poison Bomb Rune", - price = 17, - itemtype = 3173, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThis rune causes an area of 9 square metres to be contaminated with toxic gas that will poison anybody who is caught within it. Conceivable applications include the blocking of areas or the combat against fast-moving or invisible targets. Keep in mind, however, that there are a number of creatures that are immune to poison.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Poison_Wall_Rune.png" }, - name = "Poison Wall Rune", - price = 10, - itemtype = 3176, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nWhen this rune is used a wall of concentrated toxic fumes is created which inflicts a moderate poison on all those who are foolish enough to enter it. The effect is usually impressive enough to discourage monsters from doing so, although few of the stronger ones will hesitate if there is nothing but a poison wall between them and their dinner.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Soulfire_Rune.png" }, - name = "Soulfire Rune", - price = 9, - itemtype = 3195, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nSoulfire is an immensely evil spell as it directly targets a creature's very life essence. When the rune is used on a victim, its soul is temporarily moved out of its body, casting it down into the blazing fires of hell itself! Note that the experience and the mental strength of the caster influence the damage that is caused.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Stone_Shower_Rune.png" }, - name = "Stone Shower Rune", - price = 7, - itemtype = 3175, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nParticularly creatures with an affection to energy will suffer greatly from this rune filled with powerful earth damage. As the name already says, a shower of stones drums on the opponents of the rune user in an area up to 37 squares.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Sudden_Death_Rune.png" }, - name = "Sudden Death Rune", - price = 28, - itemtype = 3155, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nNearly no other spell can compare to Sudden Death when it comes to sheer damage. For this reason it is immensely popular despite the fact that only a single target is affected. However, since the damage caused by the rune is of deadly nature, it is less useful against most undead creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Thunderstorm_Rune.png" }, - name = "Thunderstorm Rune", - price = 9, - itemtype = 3202, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nFlashes filled with dangerous energy hit the rune user's opponent when this rune is being used. It is especially effective against ice dominated creatures. Covering up an area up to 37 squares, this rune is particularly useful when you meet a whole mob of opponents.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Ultimate_Healing_Rune.png" }, - name = "Ultimate Healing Rune", - price = 35, - itemtype = 3160, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nThe coveted Ultimate Healing rune is an all-time favourite among all vocations. No other healing enchantments that are bound into runes can compare to its salutary effect.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - { - icons = { "Wild_Growth_Rune.png" }, - name = "Wild Growth Rune", - price = 32, - itemtype = 3156, - count = 250, - description = "{character}\n{storeinbox}\n{vocationlevelcheck}\n{battlesign}\n{capacity}\n\nBy unleashing this spell, all seeds that are lying dormant in the surrounding quickly sprout and grow into full-sized plants, thus forming an impenetrable thicket. Unfortunately, plant life created this way is short-lived and will collapse within minutes, so the magically created obstacle will not last long.", - type = GameStore.OfferTypes.OFFER_TYPE_STACKABLE, - }, - }, - }, - -- Cosmetics - { - icons = { "Category_Cosmetics.png" }, - name = "Cosmetics", - rookgaard = true, - subclasses = { "Mounts", "Outfits" }, - }, - -- Cosmetics ~ Mounts - { - icons = { "Category_Mounts.png" }, - name = "Mounts", - parent = "Cosmetics", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Armoured_War_Horse.png" }, - name = "Armoured War Horse", - price = 870, - id = 23, - description = "{character}\n{speedboost}\n\nThe Armoured War Horse is a dangerous black beauty! When you see its threatening, blood-red eyes coming towards you, you'll know trouble is on its way. Protected by its heavy armour plates, the warhorse is the perfect partner for dangerous hunting sessions and excessive enemy slaughtering.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Arctic_Unicorn.png" }, - name = "Artic Unicorn", - price = 870, - id = 114, - description = "{character}\n{speedboost}\n\nThe Arctic Unicorn lives in a deep rivalry with its cousin the Blazing Unicorn. Even though they were born in completely different areas, they somehow share the same bloodline. The eternal battle between fire and ice continues. Who will win? Tangerine vs.crystal blue! The choice is yours!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Batcat.png" }, - name = "Batcat", - price = 870, - id = 77, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Batcat to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Battle_Badger.png" }, - name = "Battle Badger", - price = 690, - id = 147, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Black_Stag.png" }, - name = "Black Stag", - price = 660, - id = 73, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Black Stag will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Blackpelt.png" }, - name = "Blackpelt", - price = 690, - id = 58, - description = "{character}\n{speedboost}\n\nThe Blackpelt is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Blazing_Unicorn.png" }, - name = "Blazing Unicorn", - price = 870, - id = 113, - description = "{character}\n{speedboost}\n\nThe Blazing Unicorn lives in a deep rivalry with its cousin the Arctic Unicorn. Even though they were born in completely different areas, they somehow share the same bloodline. The eternal battle between fire and ice continues. Who will win? Crystal blue vs. tangerine! The choice is yours!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Bloodcurl.png" }, - name = "Bloodcurl", - price = 750, - id = 92, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Bloodcurl will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Bogwurm.png" }, - name = "Bogwurm", - price = 870, - id = 189, - description = "{character}\n{speedboost}\n\nThe Bogwurm, Gloomwurm, and Rustwurm belong to a little known subset of the dragon family, and usually live out their lives in habitats far away from human interaction. Them being cunning hunters, and their keen sense of perception make these wurms great companions for whomever can locate and tame them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Boreal_Owl.png" }, - name = "Boreal Owl", - price = 870, - id = 129, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Brass_Speckled_Koi.png" }, - name = "Brass Speckled Koi", - price = 750, - id = 208, - description = "{character}\n{speedboost}\n\nThe vibrant colours and elegance of a Tangerine Flecked Koi, a Brass Speckled Koi, and an Ink Spotted Koi make them a fascinating sight to behold, and their boisterous nature and speed will make you the first to arrive wherever there are riches to gain. Wield your weapon as gracefully and effortlessly as your swimming companion glides through the water, and the two of you will make the perfect and most deadly pair.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Bunny_Dray.png" }, - name = "Bunny Dray", - price = 870, - id = 139, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Bunny Dray comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Caped_Snowman.png" }, - name = "Caped Snowman", - price = 870, - id = 137, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Caped Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cave_Tarantula.png" }, - name = "Cave Tarantula", - price = 690, - id = 117, - description = "{character}\n{speedboost}\n\nIt is said that the Cave Tarantula was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cinderhoof.png" }, - name = "Cinderhoof", - price = 870, - id = 90, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Cinderhoof to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cinnamon_Ibex.png" }, - name = "Cinnamon Ibex", - price = 750, - id = 200, - description = "{character}\n{speedboost}\n\nNo mountain is too high, no wall too steep to climb for the agile Poppy, Mint and Cinnamon Ibex. They keep their balance on the thinnest of ledges, so you will never stumble, slip or go flying off the edges. Moreover, these sturdy fellows certainly know how to make an entrance as they dive down from the highest peaks and attack opponents with their impressive horns. And if you dare to call them a wild goat, they might kick you with their legs.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cony_Cart.png" }, - name = "Cony Cart", - price = 870, - id = 140, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Cony Cart comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Copper_Fly.png" }, - name = "Copper Fly", - price = 870, - id = 61, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Copper Fly, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Coral_Rhea.png" }, - name = "Coral Rhea", - price = 500, - id = 169, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Coralripper.png" }, - name = "Coralripper", - price = 570, - id = 79, - description = "{character}\n{speedboost}\n\nIf the Coralripper moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - --[[{ - icons = { "Corpsefire_Skull.png" }, - name = "Corpsefire Skull", - price = 750, - id = 221, - description = "{character}\n{speedboost}\n\nSkulls are the infernal heralds of untamed power. Bodies are obsolete when sinister forces animate your being. Embrace their presence and command the devastating might that awaits on the back of an eerie Corpsefire Skull!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - state = GameStore.States.STATE_NEW, - home = true, - },]] - { - icons = { "Cranium_Spider.png" }, - name = "Cranium Spider", - price = 690, - id = 116, - description = "{character}\n{speedboost}\n\nIt is said that the Cranium Spider was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Crimson_Ray.png" }, - name = "Crimson Ray", - price = 870, - id = 33, - description = "{character}\n{speedboost}\n\nHave you ever dreamed of gliding through the air on the back of a winged creature? With its deep red wings, the majestic Crimson Ray is a worthy mount for courageous heroes. Feel like a king on its back as you ride into your next adventure.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cunning_Hyaena.png" }, - name = "Cunning Hyaena", - price = 750, - id = 172, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dandelion.png" }, - name = "Dandelion", - price = 750, - id = 187, - description = "{character}\n{speedboost}\n\nBorn from the depths of the forest, where flora and fauna intertwine in mysterious ways, the Floral Beast is a colourful creature that is sure to turn some heads. The Hyacinth, Peony, and Dandelion mount are loyal companions that will safely carry you through their natural habitat of the woods, or lands unknown to them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Darkfire_Devourer.png" }, - name = "Darkfire Devourer", - price = 1300, - id = 216, - description = "{character}\n{speedboost}\n\nA wild, ancient creature, which had been hiding in the depths of the shadows for a very long time, has been spotted in Tibia again! The almighty Shadow Draptor has returned and only the bravest Tibians can control such a beast!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dawn_Strayer.png" }, - name = "Dawn Strayer", - price = 870, - id = 166, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Death_Crawler.png" }, - name = "Death Crawler", - price = 600, - id = 46, - description = "{character}\n{speedboost}\n\nThe Death Crawler is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Desert_King.png" }, - name = "Desert King", - price = 450, - id = 41, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Desert King is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - --[[{ - icons = { "Doom_Skull.png" }, - name = "Doom Skull", - price = 750, - id = 219, - description = "{character}\n{speedboost}\n\nSkulls are the infernal heralds of untamed power. Bodies are obsolete when sinister forces animate your being. Embrace their presence and command the devastating might that awaits on the back of a grim Doom Skull.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - state = GameStore.States.STATE_NEW, - home = true, - },]] - { - icons = { "Doombringer.png" }, - name = "Doombringer", - price = 780, - id = 53, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Doombringer is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dreadhare.png" }, - name = "Dreadhare", - price = 870, - id = 104, - description = "{character}\n{speedboost}\n\nDo you like fluffy bunnies but think they are too small? Do you admire the majesty of stags and their antlers but are afraid of their untameable wilderness? Do not worry, the mystic creature Dreadhare consolidates the best qualities of both animals. Hop on its backs and enjoy the ride.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Dusk_Pryer.png" }, - name = "Dusk Pryer", - price = 870, - id = 165, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ebony_Tiger.png" }, - name = "Ebony Tiger", - price = 750, - id = 123, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Ebony Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ember_Saurian.png" }, - name = "Ember Saurian", - price = 750, - id = 111, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Ember Saurian has been spotted in a sea of flames and fire deep down in the depths of Kazordoon.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emerald_Raven.png" }, - name = "Emerald Raven", - price = 690, - id = 191, - description = "{character}\n{speedboost}\n\nThe origins of the Emerald Raven, Mystic Raven, and Radiant Raven are shrouded in darkness, as no written record nor tale told by even the most knowing storytellers mentions but a trace of them. Superstition surrounds them, as some see these gigantic birds as an echo of a long forgotten past, while others believe them to herald hitherto unknown events. What is clear is that they are highly intelligent beings which make great companions if they deem somebody worthy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emerald_Sphinx.png" }, - name = "Emerald Sphinx", - price = 750, - id = 108, - description = "{character}\n{speedboost}\n\nRide an Emerald Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emerald_Waccoon.png" }, - name = "Emerald Waccoon", - price = 750, - id = 70, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Emerald Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Emperor_Deer.png" }, - name = "Emperor Deer", - price = 660, - id = 74, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Emperor Deer will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ether_Badger.png" }, - name = "Ether Badger", - price = 690, - id = 148, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Eventide_Nandu.png" }, - name = "Eventide Nandu", - price = 500, - id = 170, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Feral_Tiger.png" }, - name = "Feral Tiger", - price = 750, - id = 124, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Feral Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Festive_Mammoth.png" }, - name = "Festive Mammoth", - price = 750, - id = 178, - description = "{character}\n{speedboost}\n\nThe Festive Mammoth, Holiday Mammoth and Merry Mammoth are gentle giants with a massive appearance and impressive tusks, whose mission it is to deliver gifts all across Tibia. They are good-natured beings, spreading joy wherever they go, but you best not cross them - a mammoth never forgets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Festive_Snowman.png" }, - name = "Festive Snowman", - price = 900, - id = 135, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Festive Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flamesteed.png" }, - name = "Flamesteed", - price = 900, - id = 47, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Flamesteed has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flitterkatzen.png" }, - name = "Flitterkatzen", - price = 870, - id = 75, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Flitterkatzen to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Augur.png" }, - name = "Floating Augur", - price = 870, - id = 155, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Kashmir.png" }, - name = "Floating Kashmir", - price = 900, - id = 67, - description = "{character}\n{speedboost}\n\nThe Floating Kashmir is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Sage.png" }, - name = "Floating Sage", - price = 870, - id = 153, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Floating_Scholar.png" }, - name = "Floating Scholar", - price = 870, - id = 154, - description = "{character}\n{speedboost}\n\nThese creatures are Floating Savants whose mind has been warped and bent to focus their extraordinary mental capabilities on one single goal: to do their master's bidding. Instead of being filled with an endless pursuit of knowledge, their live is now one of continuous thralldom and serfhood. The Floating Sage, the Floating Scholar and the Floating Augur are at your disposal.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Flying_Divan.png" }, - name = "Flying Divan", - price = 900, - id = 65, - description = "{character}\n{speedboost}\n\nThe Flying Divan is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Foxmouse.png" }, - name = "Foxmouse", - price = 750, - id = 218, - description = "{character}\n{speedboost}\n\nA wild, ancient creature, which had been hiding in the depths of the shadows for a very long time, has been spotted in Tibia again! The almighty Shadow Draptor has returned and only the bravest Tibians can control such a beast!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Frostbringer.png" }, - name = "Frostbringer", - price = 750, - id = 210, - description = "{character}\n{speedboost}\n\nTenacity, strength and loyalty are the hallmarks of a Frostbringer, a Winterstride or an Icebreacher. Those travelling through barren lands, pursuing goals in forbidding environments, or simply wanting a comrade for a lifetime should fall back on this stalwart companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Frostflare.png" }, - name = "Frostflare", - price = 870, - id = 89, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Frostflare to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Glacier_Vagabond.png" }, - name = "Glacier Vagabond", - price = 750, - id = 64, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Glacier Vagabond will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gloom_Widow.png" }, - name = "Gloom Widow", - price = 690, - id = 118, - description = "{character}\n{speedboost}\n\nIt is said that the Gloom Widow was born long before Banor walked the earth of Tibia. While its parents died in the war against the cruel hordes sent by Brog and Zathroth, their child survived by hiding in skulls of burned enemies. It never left its hiding spot and as it grew older, the skulls merged into its body. Now, it is fully-grown and thirsts for revenge.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gloomwurm.png" }, - name = "Gloomwurm", - price = 870, - id = 190, - description = "{character}\n{speedboost}\n\nThe Bogwurm, Gloomwurm, and Rustwurm belong to a little known subset of the dragon family, and usually live out their lives in habitats far away from human interaction. Them being cunning hunters, and their keen sense of perception make these wurms great companions for whomever can locate and tame them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gold_Sphinx.png" }, - name = "Gold Sphinx", - price = 750, - id = 107, - description = "{character}\n{speedboost}\n\nRide a Gold Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Golden_Dragonfly.png" }, - name = "Golden Dragonfly", - price = 600, - id = 59, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Golden Dragonfly, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Gorongra.png" }, - name = "Gorongra", - price = 720, - id = 81, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Gorongra is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Hailstorm_Fury.png" }, - name = "Hailtorm Fury", - price = 780, - id = 55, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Hailstorm Fury is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Highland_Yak.png" }, - name = "Highland Yak", - price = 750, - id = 63, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Highland Yak will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Holiday_Mammoth.png" }, - name = "Holiday Mammoth", - price = 750, - id = 177, - description = "{character}\n{speedboost}\n\nThe Festive Mammoth, Holiday Mammoth and Merry Mammoth are gentle giants with a massive appearance and impressive tusks, whose mission it is to deliver gifts all across Tibia. They are good-natured beings, spreading joy wherever they go, but you best not cross them - a mammoth never forgets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Hyacinth.png" }, - name = "Hyacinth", - price = 750, - id = 185, - description = "{character}\n{speedboost}\n\nBorn from the depths of the forest, where flora and fauna intertwine in mysterious ways, the Floral Beast is a colourful creature that is sure to turn some heads. The Hyacinth, Peony, and Dandelion mount are loyal companions that will safely carry you through their natural habitat of the woods, or lands unknown to them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Icebreacher.png" }, - name = "Icebreacher", - price = 750, - id = 212, - description = "{character}\n{speedboost}\n\nTenacity, strength and loyalty are the hallmarks of a Frostbringer, a Winterstride or an Icebreacher. Those travelling through barren lands, pursuing goals in forbidding environments, or simply wanting a comrade for a lifetime should fall back on this stalwart companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ink_Spotted_Koi.png" }, - name = "Ink Spotted Koi", - price = 750, - id = 209, - description = "{character}\n{speedboost}\n\nThe vibrant colours and elegance of a Tangerine Flecked Koi, a Brass Speckled Koi, and an Ink Spotted Koi make them a fascinating sight to behold, and their boisterous nature and speed will make you the first to arrive wherever there are riches to gain. Wield your weapon as gracefully and effortlessly as your swimming companion glides through the water, and the two of you will make the perfect and most deadly pair.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ivory_Fang.png" }, - name = "Ivory Fang", - price = 750, - id = 100, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Ivory Fang, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jackalope.png" }, - name = "Jackalope", - price = 870, - id = 103, - description = "{character}\n{speedboost}\n\nDo you like fluffy bunnies but think they are too small? Do you admire the majesty of stags and their antlers but are afraid of their untameable wilderness? Do not worry, the mystic creature Jackalope consolidates the best qualities of both animals. Hop on its backs and enjoy the ride.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jade_Lion.png" }, - name = "Jade Lion", - price = 450, - id = 48, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Jade Lion is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jade_Pincer.png" }, - name = "Jade Pincer", - price = 600, - id = 49, - description = "{character}\n{speedboost}\n\nThe Jade Pincer is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jade_Shrine.png" }, - name = "Jade Shrine", - price = 690, - id = 196, - description = "{character}\n{speedboost}\n\nThe famous Wandering Shrines were first raised by the nomad people of the Zaoan steppe. Their exceptional craftsmanship, combining architectonic features with living animals, is acknowledged even far beyond the continent of Zao. These spiritual companions will give you the opportunity to regain your strength during long and exciting journeys.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jousting_Horse.png" }, - name = "Jousting Horse", - price = 870, - id = 204, - description = "{character}\n{speedboost}\n\nA seasoned warrior knows how to make an entry, and so does his faithful companion: Fully armored! Saddle up your impressive Jousting Horse to charge into battle in style, gallop into the arena on the back of your striking Tourney Horse, and ride your distinguished Parade Horse through the streets of Thais to show off your chivalrous qualities. With a horse in full barding, nobody will ever rain on your parade again.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jungle_Saurian.png" }, - name = "Jungle Saurian", - price = 750, - id = 110, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Jungle Saurian likes to hide in dense wood and overturned trees.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jungle_Tiger.png" }, - name = "Jungle Tiger", - price = 750, - id = 125, - description = "{character}\n{speedboost}\n\nIt is said that in ancient times, the sabre-tooth tiger was already used as a mount by elder warriors of Svargrond. As seafaring began to expand, this noble big cat was also transported to other regions in Tibia. Influenced by the new environment and climatic changes, the fur of the Jungle Tiger has developed its extraordinary colouring over several generations.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Lagoon_Saurian.png" }, - name = "Lagoon Saurian", - price = 750, - id = 112, - description = "{character}\n{speedboost}\n\nThousands of years ago, its ancestors ruled the world. Only recently, it found its way into Tibia. The Lagoon Saurian feels most comfortable in torrential rivers and behind dangerous waterfalls.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Leafscuttler.png" }, - name = "Leafscuttler", - price = 750, - id = 93, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Leafscuttler will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Magic_Carpet.png" }, - name = "Magic Carpet", - price = 900, - id = 66, - description = "{character}\n{speedboost}\n\nThe Magic Carpet is the perfect mount for those who are too busy to take care of an animal mount or simply like to travel on a beautiful, magic hand-woven carpet.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - --[[{ - icons = { "Magma_Skull.png" }, - name = "Magma Skull", - price = 750, - id = 220, - description = "{character}\n{speedboost}\n\nSkulls are the infernal heralds of untamed power. Bodies are obsolete when sinister forces animate your being. Embrace their presence and command the devastating might that awaits on the back of a blazing Magma Skull.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - state = GameStore.States.STATE_NEW, - home = true, - },]] - { - icons = { "Marsh_Toad.png" }, - name = "Marsh Toad", - price = 690, - id = 120, - description = "{character}\n{speedboost}\n\nThe Magic Carpet is the perfect mount for those who are too busy to take cFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Marsh Toad a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Merry_Mammoth.png" }, - name = "Merry Mammoth", - price = 750, - id = 176, - description = "{character}\n{speedboost}\n\nThe Festive Mammoth, Holiday Mammoth and Merry Mammoth are gentle giants with a massive appearance and impressive tusks, whose mission it is to deliver gifts all across Tibia. They are good-natured beings, spreading joy wherever they go, but you best not cross them - a mammoth never forgets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mint_Ibex.png" }, - name = "Mint Ibex", - price = 750, - id = 199, - description = "{character}\n{speedboost}\n\nNo mountain is too high, no wall too steep to climb for the agile Poppy, Mint and Cinnamon Ibex. They keep their balance on the thinnest of ledges, so you will never stumble, slip or go flying off the edges. Moreover, these sturdy fellows certainly know how to make an entrance as they dive down from the highest peaks and attack opponents with their impressive horns. And if you dare to call them a wild goat, they might kick you with their legs.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mould_Shell.png" }, - name = "Mould Shell", - price = 690, - id = 96, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Mould Shell might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mouldpincer.png" }, - name = "Mouldpincer", - price = 750, - id = 91, - description = "{character}\n{speedboost}\n\nYou are fascinated by insectoid creatures and can picture yourself riding one during combat or just for travelling? The Mouldpincer will carry you through the Tibian wilderness with ease.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Muffled_Snowman.png" }, - name = "Muffled Snowman", - price = 900, - id = 136, - description = "{character}\n{speedboost}\n\nWhen the nights are getting longer and freezing wind brings driving snow into the land, snowmen rise and shine on every corner. Lately, a peaceful, arcane creature has found shelter in one of them and used its magical power to call the Muffled Snowman into being. Wrap yourself up well and warmly and jump on the back of your new frosty companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Mystic_Raven.png" }, - name = "Mystic Raven", - price = 690, - id = 192, - description = "{character}\n{speedboost}\n\nThe origins of the Emerald Raven, Mystic Raven, and Radiant Raven are shrouded in darkness, as no written record nor tale told by even the most knowing storytellers mentions but a trace of them. Superstition surrounds them, as some see these gigantic birds as an echo of a long forgotten past, while others believe them to herald hitherto unknown events. What is clear is that they are highly intelligent beings which make great companions if they deem somebody worthy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nethersteed.png" }, - name = "Nethersteed", - price = 900, - id = 50, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Nethersteed has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Night_Waccoon.png" }, - name = "Night Waccoon", - price = 750, - id = 69, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Night Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightdweller.png" }, - name = "Nightdweller", - price = 870, - id = 88, - description = "{character}\n{speedboost}\n\nIf you are more of an imp than an angel, you may prefer riding out on a Nightdweller to scare fellow Tibians on their festive strolls. Its devilish mask, claw-like hands and sharp hooves makes it the perfect companion for any daring adventurer who likes to stand out.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightmarish_Crocovile.png" }, - name = "Nightmarish Crocovile", - price = 750, - id = 143, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Nightstinger.png" }, - name = "Nightstinger", - price = 780, - id = 85, - description = "{character}\n{speedboost}\n\nThe Nightstinger has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Noctungra.png" }, - name = "Noctungra", - price = 720, - id = 82, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Noctungra is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Obsidian_Shrine.png" }, - name = "Obsidian Shrine", - price = 690, - id = 197, - description = "{character}\n{speedboost}\n\nThe famous Wandering Shrines were first raised by the nomad people of the Zaoan steppe. Their exceptional craftsmanship, combining architectonic features with living animals, is acknowledged even far beyond the continent of Zao. These spiritual companions will give you the opportunity to regain your strength during long and exciting journeys.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Parade_Horse.png" }, - name = "Parade Horse", - price = 870, - id = 203, - description = "{character}\n{speedboost}\n\nA seasoned warrior knows how to make an entry, and so does his faithful companion: Fully armored! Saddle up your impressive Jousting Horse to charge into battle in style, gallop into the arena on the back of your striking Tourney Horse, and ride your distinguished Parade Horse through the streets of Thais to show off your chivalrous qualities. With a horse in full barding, nobody will ever rain on your parade again.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Peony.png" }, - name = "Peony", - price = 750, - id = 186, - description = "{character}\n{speedboost}\n\nBorn from the depths of the forest, where flora and fauna intertwine in mysterious ways, the Floral Beast is a colourful creature that is sure to turn some heads. The Hyacinth, Peony, and Dandelion mount are loyal companions that will safely carry you through their natural habitat of the woods, or lands unknown to them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Plumfish.png" }, - name = "Plumfish", - price = 570, - id = 80, - description = "{character}\n{speedboost}\n\nIf the Plumfish moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Poisonbane.png" }, - name = "Poisonbane", - price = 690, - id = 57, - description = "{character}\n{speedboost}\n\nThe Poisonbane is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Poppy_Ibex.png" }, - name = "Poppy Ibex", - price = 750, - id = 198, - description = "{character}\n{speedboost}\n\nNo mountain is too high, no wall too steep to climb for the agile Poppy, Mint and Cinnamon Ibex. They keep their balance on the thinnest of ledges, so you will never stumble, slip or go flying off the edges. Moreover, these sturdy fellows certainly know how to make an entrance as they dive down from the highest peaks and attack opponents with their impressive horns. And if you dare to call them a wild goat, they might kick you with their legs.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Prismatic_Unicorn.png" }, - name = "Prismatic Unicorn", - price = 870, - id = 115, - description = "{character}\n{speedboost}\n\nLegend has it that a mare and a stallion once reached the end of a rainbow and decided to stay there. Influenced by the mystical power of the rainbow, the mare gave birth to an exceptional foal: Not only the big, strong horn on its forehead but the unusual colouring of its hair makes the Prismatic Unicorn a unique mount in every respect.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Rabbit_Rickshaw.png" }, - name = "Rabbit Rickshaw", - price = 870, - id = 138, - description = "{character}\n{speedboost}\n\nYour lower back worsens with every trip you spend on the back of your mount and you are looking for a more comfortable alternative to travel through the lands? Say no more! The Rabbit Rickshaw comes with two top-performing hares that never get tired thanks to the brand new and highly innovative propulsion technology. Just keep some back-up carrots in your pocket and you will be fine!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Radiant_Raven.png" }, - name = "Radiant Raven", - price = 690, - id = 193, - description = "{character}\n{speedboost}\n\nThe origins of the Emerald Raven, Mystic Raven, and Radiant Raven are shrouded in darkness, as no written record nor tale told by even the most knowing storytellers mentions but a trace of them. Superstition surrounds them, as some see these gigantic birds as an echo of a long forgotten past, while others believe them to herald hitherto unknown events. What is clear is that they are highly intelligent beings which make great companions if they deem somebody worthy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Razorcreep.png" }, - name = "Razorcreep", - price = 780, - id = 86, - description = "{character}\n{speedboost}\n\nThe Razorcreep has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Reed_Lurker.png" }, - name = "Reed Lurker", - price = 690, - id = 97, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Reed Lurker might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Rift_Watcher.png" }, - name = "Rift Watcher", - price = 870, - id = 181, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Rift Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Ringtail_Waccoon.png" }, - name = "Ringtail Waccoon", - price = 750, - id = 68, - description = "{character}\n{speedboost}\n\nWaccoons are cuddly creatures that love nothing more than to be petted and snuggled! Share a hug, ruffle the fur of the Ringtail Waccoon and scratch it behind its ears to make it happy.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "River_Crocovile.png" }, - name = "River Crocovile", - price = 750, - id = 141, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Rune_Watcher.png" }, - name = "Rune Watcher", - price = 870, - id = 180, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Rune Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Rustwurm.png" }, - name = "Rustwurm", - price = 870, - id = 188, - description = "{character}\n{speedboost}\n\nThe Bogwurm, Gloomwurm, and Rustwurm belong to a little known subset of the dragon family, and usually live out their lives in habitats far away from human interaction. Them being cunning hunters, and their keen sense of perception make these wurms great companions for whomever can locate and tame them.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Sanguine_Frog.png" }, - name = "Sanguine Frog", - price = 690, - id = 121, - description = "{character}\n{speedboost}\n\nFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Sanguine Frog a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Savanna_Ostrich.png" }, - name = "Savanna Ostrich", - price = 500, - id = 168, - description = "{character}\n{speedboost}\n\nThese birds have a strong maternal instinct since their fledglings are completely dependent on their parents for protection. Do not expect them to abandon their brood only because they are carrying you around. In fact, if you were to separate them from their chick, the Savanna Ostrich, Coral Rhea and Eventide Nandu would turn into vicious beings, so don't even try it!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Scruffy_Hyaena.png" }, - name = "Scruffy Hyaena", - price = 750, - id = 173, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Sea_Devil.png" }, - name = "Sea Devil", - price = 570, - id = 78, - description = "{character}\n{speedboost}\n\nIf the Sea Devil moves its fins, it generates enough air pressure that it can even float over land. Its numerous eyes allow it to quickly detect dangers even in confusing situations and eliminate them with one powerful bite. If you watch your fingers, you are going to be good friends.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Claw.png" }, - name = "Shadow Claw", - price = 750, - id = 101, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Shadow Claw, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Draptor.png" }, - name = "Shadow Draptor", - price = 870, - id = 24, - description = "{character}\n{speedboost}\n\nA wild, ancient creature, which had been hiding in the depths of the shadows for a very long time, has been spotted in Tibia again! The almighty Shadow Draptor has returned and only the bravest Tibians can control such a beast!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Hart.png" }, - name = "Shadow Hart", - price = 660, - id = 72, - description = "{character}\n{speedboost}\n\nTreat your character to a new travelling companion with a gentle nature and an impressive antler: The noble Shadow Hart will carry you through the deepest snow.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Shadow_Sphinx.png" }, - name = "Shadow Sphinx", - price = 750, - id = 109, - description = "{character}\n{speedboost}\n\nRide a Shadow Sphinx on your way through ancient chambers and tombs and have a loyal friend by your side while fighting countless mummies and other creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Siegebreaker.png" }, - name = "Siegebreaker", - price = 690, - id = 56, - description = "{character}\n{speedboost}\n\nThe Siegebreaker is out searching for the best bamboo in Tibia. Its heavy armour allows it to visit even the most dangerous places. Treat it nicely with its favourite food from time to time and it will become a loyal partner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Silverneck.png" }, - name = "Silverneck", - price = 720, - id = 83, - description = "{character}\n{speedboost}\n\nGet yourself a mighty travelling companion with broad shoulders and a gentle heart. Silverneck is a physically imposing creature that is much more peaceful than its relatives, Tiquanda's wild kongras, and will carry you safely wherever you ask it to go.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Slagsnare.png" }, - name = "Slagsnare", - price = 780, - id = 84, - description = "{character}\n{speedboost}\n\nThe Slagsnare has external characteristics of different breeds. It is assumed that his brain is also composed of many different species, which makes it completely unpredictable. Only few have managed to approach this creature unharmed and only the best could tame it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snow_Pelt.png" }, - name = "Snow Pelt", - price = 750, - id = 102, - description = "{character}\n{speedboost}\n\nIncredible strength and smartness, an irrepressible will to survive, passionately hunting in groups. If these attributes apply to your character, we have found the perfect partner for you. Have a proper look at Snow Pelt, which stands loyally by its master's side in every situation. It is time to become the leader of the wolf pack!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snow_Strider.png" }, - name = "Snow Strider", - price = 870, - id = 164, - description = "{character}\n{speedboost}\n\nA magical fire burns inside these wolves. Bred as the faithful guardians for an eccentric wizard's tower, these creatures make for loyal companions during your travels. While not originally intended for riding, their sturdy frame makes the Dawn Strayer, Dusk Pryer and Snow Strider suitable mounts.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Snowy_Owl.png" }, - name = "Snowy Owl", - price = 870, - id = 128, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Spirit_of_Purity.png" }, - name = "Spirit of Purity", - price = 1000, - id = 217, - description = "{character}\n{speedboost}\n\nA wild, ancient creature, which had been hiding in the depths of the shadows for a very long time, has been spotted in Tibia again! The almighty Shadow Draptor has returned and only the bravest Tibians can control such a beast!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Steel_Bee.png" }, - name = "Steel Bee", - price = 600, - id = 60, - description = "{character}\n{speedboost}\n\nIf you are more interested in the achievements of science, you may enjoy a ride on the Steel Bee, one of the new insect-like flying machines. Even if you do not move around, the wings of these unusual vehicles are always in motion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Steelbeak.png" }, - name = "Steelbeak", - price = 870, - id = 34, - description = "{character}\n{speedboost}\n\nForged by only the highest skilled blacksmiths in the depths of Kazordoon's furnaces, a wild animal made out of the finest steel arose from glowing embers and blazing heat. Protected by its impenetrable armour, the Steelbeak is ready to accompany its master on every battleground.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Swamp_Crocovile.png" }, - name = "Swamp Crocovile", - price = 750, - id = 142, - description = "{character}\n{speedboost}\n\nTo the keen observer, the crocovile is clearly a relative of the crocodile, albeit their look suggests an even more aggressive nature. While it is true that the power of its massive and muscular body can not only crush enemies dead but also break through any gate like a battering ram, a crocovile is, above all, a steadfast companion showing unwavering loyalty to its owner.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Swamp_Snapper.png" }, - name = "Swamp Snapper", - price = 690, - id = 95, - description = "{character}\n{speedboost}\n\nYou are intrigued by tortoises and would love to throne on a tortoise shell when travelling the Tibian wilderness? The Swamp Snapper might become your new trustworthy companion then, which will transport you safely and even carry you during combat.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tangerine_Flecked_Koi.png" }, - name = "Tangerine Speckled Koi", - price = 750, - id = 207, - description = "{character}\n{speedboost}\n\nThe vibrant colours and elegance of a Tangerine Flecked Koi, a Brass Speckled Koi, and an Ink Spotted Koi make them a fascinating sight to behold, and their boisterous nature and speed will make you the first to arrive wherever there are riches to gain. Wield your weapon as gracefully and effortlessly as your swimming companion glides through the water, and the two of you will make the perfect and most deadly pair.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tawny_Owl.png" }, - name = "Tawny Owl", - price = 870, - id = 127, - description = "{character}\n{speedboost}\n\nOwls have always been a symbol of mystery, magic and wisdom in Tibian myths and fairy tales. Having one of these enigmatic creatures of the night as a trustworthy companion provides you with a silent guide whose ever-watchful eyes will cut through the shadows, help you navigate the darkness and unravel great secrets.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tempest.png" }, - name = "Tempest", - price = 900, - id = 51, - description = "{character}\n{speedboost}\n\nOnce a majestic and proud warhorse, the Tempest has fallen in a horrible battle many years ago. Driven by agony and pain, its spirit once again took possession of its rotten corpse to avenge its death. Stronger than ever, it seeks a master to join the battlefield, aiming for nothing but death and destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tombstinger.png" }, - name = "Tombstinger", - price = 600, - id = 36, - description = "{character}\n{speedboost}\n\nThe Tombstinger is a scorpion that has surpassed the natural boundaries of its own kind. Way bigger, stronger and faster than ordinary scorpions, it makes a perfect companion for fearless heroes and explorers. Just be careful of his poisonous sting when you mount it.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Topaz_Shrine.png" }, - name = "Topaz Shrine", - price = 690, - id = 195, - description = "{character}\n{speedboost}\n\nThe famous Wandering Shrines were first raised by the nomad people of the Zaoan steppe. Their exceptional craftsmanship, combining architectonic features with living animals, is acknowledged even far beyond the continent of Zao. These spiritual companions will give you the opportunity to regain your strength during long and exciting journeys.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tourney_Horse.png" }, - name = "Tourney Horse", - price = 870, - id = 205, - description = "{character}\n{speedboost}\n\nA seasoned warrior knows how to make an entry, and so does his faithful companion: Fully armored! Saddle up your impressive Jousting Horse to charge into battle in style, gallop into the arena on the back of your striking Tourney Horse, and ride your distinguished Parade Horse through the streets of Thais to show off your chivalrous qualities. With a horse in full barding, nobody will ever rain on your parade again.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Toxic_Toad.png" }, - name = "Toxic Toad", - price = 690, - id = 122, - description = "{character}\n{speedboost}\n\nFor centuries, humans and monsters have dumped their garbage in the swamps around Venore. The combination of old, rusty weapons, stale mana and broken runes have turned some of the swamp dwellers into gigantic frogs. Benefit from those mutations and make the Toxic Toad a faithful mount for your adventures even beyond the bounds of the swamp.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Tundra_Rambler.png" }, - name = "Tundra Rambler", - price = 750, - id = 62, - description = "{character}\n{speedboost}\n\nWith its thick, shaggy hair, the Tundra Rambler will keep you warm even in the chilly climate of the Ice Islands. Due to its calm and peaceful nature, it is not letting itself getting worked up easily.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Venompaw.png" }, - name = "Venompaw", - price = 870, - id = 76, - description = "{character}\n{speedboost}\n\nRumour has it that many years ago elder witches had gathered to hold a magical feast high up in the mountains. They had crossbred Venompaw to easily conquer rocky canyons and deep valleys. Nobody knows what happened on their way up but only the mount has been seen ever since.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Void_Watcher.png" }, - name = "Void Watcher", - price = 870, - id = 179, - description = "{character}\n{speedboost}\n\nIf you are looking for a vigilant and faithful companion, look no further! Glide through every realm and stare into the darkest abyss on the back of a Void Watcher. They already know everything about you anyway for they have been watching you from the shadows!", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Voracious_Hyaena.png" }, - name = "Voracious Hyaena", - price = 750, - id = 171, - description = "{character}\n{speedboost}\n\nThe Cunning Hyaena, Scruffy Hyaena and Voracious Hyaena are highly social animals and loyal companions to whomever is able to befriend them. Coming from sun-soaked places, they prefer a warm climate, but are able to cope in other environments as well.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Winter_King.png" }, - name = "Winter King", - price = 450, - id = 52, - description = "{character}\n{speedboost}\n\nIts roaring is piercing marrow and bone and can be heard over ten miles away. The Winter King is the undisputed ruler of its territory and no one messes with this animal. Show no fear and prove yourself worthy of its trust and you will get yourself a valuable companion for your adventures.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Winterstride.png" }, - name = "Winterstride", - price = 750, - id = 211, - description = "{character}\n{speedboost}\n\nTenacity, strength and loyalty are the hallmarks of a Frostbringer, a Winterstride or an Icebreacher. Those travelling through barren lands, pursuing goals in forbidding environments, or simply wanting a comrade for a lifetime should fall back on this stalwart companion.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Wolpertinger.png" }, - name = "Wolpertinger", - price = 870, - id = 105, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Woodland Prince is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Woodland_Prince.png" }, - name = "Woodland Prince", - price = 780, - id = 54, - description = "{character}\n{speedboost}\n\nOnce captured and held captive by a mad hunter, the Woodland Prince is the result of sick experiments. Fed only with demon dust and concentrated demonic blood it had to endure a dreadful transformation. The demonic blood that is now running through its veins, however, provides it with incredible strength and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Zaoan_Badger.png" }, - name = "Zaoan Badger", - price = 690, - id = 149, - description = "{character}\n{speedboost}\n\nBadgers have been a staple of the Tibian fauna for a long time, and finally some daring souls have braved the challenge to tame some exceptional specimens - and succeeded! While the common badger you can encounter during your travels might seem like a rather unassuming creature, the Battle Badger, the Ether Badger, and the Zaoan Badger are fierce and mighty beasts, which are at your beck and call.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - }, - }, - -- Cosmetics ~ Outfits (base outfit has addon = 0 or no defined addon. By default addon is set to 0) - { - icons = { "Category_Outfits.png" }, - name = "Outfits", - parent = "Cosmetics", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Outfit_Arbalester_Male_Addon_3.png", "Outfit_Arbalester_Female_Addon_3.png" }, - name = "Full Arbalester Outfit", - price = 600, - sexId = { female = 1450, male = 1449 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nArmed with a powerful crossbow, and gifted with steady hands as well as a sharp eye, the Arbalester is not one to be trifled with. Requiring both skill and strength to properly wield, the arbalest is a mighty tool in the hands of an able marksman, shooting deadly bolts across great distance.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Arena_Champion_Male_Addon_3.png", "Outfit_Arena_Champion_Female_Addon_3.png" }, - name = "Full Arena Champion Outfit", - price = 870, - sexId = { female = 885, male = 884 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFight your bloody battles in the arena and become a darling of the crowd. Once you have made it to the top and everyone is cheering your name, the fashionable outfit of an Arena Champion will show the world what you are made of.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Armoured_Archer_Male_Addon_3.png", "Outfit_Armoured_Archer_Female_Addon_3.png" }, - name = "Full Armoured Archer Outfit", - price = 600, - sexId = { female = 1619, male = 1618 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nArmoured Archers are the epitome of invisible danger. Silently and nimbly, they advance in the background. For hours, they wait patiently, almost motionless, for the decisive moment. Just to be perfectly present in a deadly second.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - home = true, - }, - { - icons = { "Outfit_Beastmaster_Male_Addon_3.png", "Outfit_Beastmaster_Female_Addon_3.png" }, - name = "Full Beastmaster Outfit", - price = 870, - sexId = { female = 636, male = 637 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you have enough authority to make wild animals subservient to you? Become a Beastmaster and surround yourself with fearsome companions. When your beasts bare their teeth, your enemies will turn tails and run.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Breezy_Garb_Male_Addon_3.png", "Outfit_Breezy_Garb_Female_Addon_3.png" }, - name = "Full Breezy Garb Outfit", - price = 600, - sexId = { female = 1246, male = 1245 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nEven the most eager adventurers and toughest warriors need some time to rest and recharge. Enjoy tranquility and peace as you picnic in good company at one of your favourite places in Tibia. Put on your Breezy Garb outfit, grab your walking stick, a basket filled with tasty snacks and then head out into nature!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Ceremonial_Garb_Male_Addon_3.png", "Outfit_Ceremonial_Garb_Female_Addon_3.png" }, - name = "Full Ceremonial Garb Outfit", - price = 750, - sexId = { female = 694, male = 695 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nIf you want to make a great entrance at a Tibian costume party, the Ceremonial Garb is certainly a good choice. With a drum over your shoulder and adorned with feathers you are perfectly dressed to lead a carnival parade through the streets of Thais.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Champion_Male_Addon_3.png", "Outfit_Champion_Female_Addon_3.png" }, - name = "Full Champion Outfit", - price = 570, - sexId = { female = 632, male = 633 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nProtect your body with heavy armour plates and spiky bones to teach your enemies the meaning of fear! The Champion outfit perfectly suits battle-hardened warriors who rely on their trusty sword and shield.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Chaos_Acolyte_Male_Addon_3.png", "Outfit_Chaos_Acolyte_Female_Addon_3.png" }, - name = "Full Chaos Acolyte Outfit", - price = 900, - sexId = { female = 664, male = 665 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou have always felt like the cat among the pigeons and have a fable for dark magic? The Chaos Acolyte outfit is a perfect way to express your inner nature. Show your commitment for the higher cause and wreak havoc on your enemies in this unique outfit.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Conjurer_Male_Addon_3.png", "Outfit_Conjurer_Female_Addon_3.png" }, - name = "Full Conjurer Outfit", - price = 750, - sexId = { female = 635, male = 634 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou recently graduated from the Magic Academy and want to bring your knowledge to good use? Congratulations, you are now an honourable disciple of magic! Open up a bottle of well-aged mana and treat yourself with the fashionable Conjurer outfit.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Death_Herald_Male_Addon_3.png", "Outfit_Death_Herald_Female_Addon_3.png" }, - name = "Full Death Herald Outfit", - price = 600, - sexId = { female = 666, male = 667 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDeath and decay are your ever-present companions? Your enemies are dropping like flies and your path is covered with their bodies? However, as decency demands, you want to at least give them a proper funeral? Then the Death Herald is just the right outfit for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Dragon_Knight_Male_Addon_3.png", "Outfit_Dragon_Knight_Female_Addon_3.png" }, - name = "Full Dragon Knight Outfit", - price = 870, - sexId = { female = 1445, male = 1444 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Dragon Knight is ready for everything, channeling the primordial might of the winged, ancient beasts into weapons and armour. Their imposing demeanour and impressive appearance are often enough to quell any animosity towards them, and those who still dare oppose them are not long for this world.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Entrepreneur_Male_Addon_3.png", "Outfit_Entrepreneur_Female_Addon_3.png" }, - name = "Full Entrepreneur Outfit", - price = 750, - sexId = { female = 471, male = 472 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nSlaughter through hordes of monsters during your early morning hunt and kiss the hand of Queen Eloise later on at the evening reception in her historical residence. With the Entrepreneur outfit you will cut a fine figure on every occasion.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Evoker_Male_Addon_3.png", "Outfit_Evoker_Female_Addon_3.png" }, - name = "Full Evoker Outfit", - price = 840, - sexId = { female = 724, male = 725 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDance around flickering fires in the Evoker outfit while singing unholy chants to praise witchcraft and wizardry. Your faithful bat will always be by your side.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Fencer_Male_Addon_3.png", "Outfit_Fencer_Female_Addon_3.png" }, - name = "Full Fencer Outfit", - price = 750, - sexId = { female = 1576, male = 1575 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThey are skilled, they are disciplined, they wield their weapon with deadly precision as a form of art. Fencers are true masters of the blade who can cut through anything and anyone in the blink of an eye. While being feared for their lethal attacks, they are also admired for their elegant and fierce style, their dashing looks. Do not be on the fence, be a fencer, or at least dress like one with this fashionable, cutting-edge outfit.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Flamefury_Mage_Male_Addon_3.png", "Outfit_Flamefury_Mage_Female_Addon_3.png" }, - name = "Full Flamefury Mage Outfit", - price = 870, - sexId = { female = 1681, male = 1680 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFlame fury mages not only embraced the fury to fire, they became it! Relishing in purging destruction they don't just want to see the world burn, but be an active part in it. They just love the smell of sulphur in the morning!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Forest_Warden_Male_Addon_3.png", "Outfit_Forest_Warden_Female_Addon_3.png" }, - name = "Full Forest Warden Outfit", - price = 750, - sexId = { female = 1416, male = 1415 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Forest Warden watches over all living things in the woods, be they plants or beasts. They have a special connection to the earth they tread on, the air they breathe, and the wind which whispers around them. Naturally, the suit that they don is not made out of dead vegetation, but is a living being itself.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Frost_Tracer_Male_Addon_3.png", "Outfit_Frost_Tracer_Female_Addon_3.png" }, - name = "Full Frost Tracer Outfit", - price = 750, - sexId = { female = 1613, male = 1612 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nTheir imposing appearance alone made many experienced warriors shudder. Frost Tracers are true giants that are used to survive even under the most inhospitable circumstances. Tough, strong and untamable, they follow their own path through roughness and obscurity. Nobody wants to mess with one of their kind.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Ghost_Blade_Male_Addon_3.png", "Outfit_Ghost_Blade_Female_Addon_3.png" }, - name = "Full Ghost Blade Outfit", - price = 600, - sexId = { female = 1490, male = 1489 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nBeing a Ghost Blade means having mastered the way of the warrior. No matter the circumstances, these fighters retain full control over their body and mind, with the sole focus of vanquishing their foe. So great is their ability that they not only control the weapons in their hands perfectly, but two floating blades following them as well.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Grove_Keeper_Male_Addon_3.png", "Outfit_Grove_Keeper_Female_Addon_3.png" }, - name = "Full Groove Keeper Outfit", - price = 870, - sexId = { female = 909, male = 908 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFeeling the springy grass under your feet and inhaling the spicy air of the forest is pure satisfaction for your soul? Every animal is your friend and you caringly look after trees and plants all the time? Then it is time to become one with nature: Become a Grove Keeper!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Guidon_Bearer_Male_Addon_3.png", "Outfit_Guidon_Bearer_Female_Addon_3.png" }, - name = "Full Guidon Bearer Outfit", - price = 870, - sexId = { female = 1187, male = 1186 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nCarrying the guidon of a unit, always marching in front, is not only an honour but also comes with great responsibility. Guidon bearers wield great power, they lead where others follow and keep the spirits of the troops up as they wave their flag against the golden suns of Tibia.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Herbalist_Male_Addon_3.png", "Outfit_Herbalist_Female_Addon_3.png" }, - name = "Full Herbalist Outfit", - price = 750, - sexId = { female = 1020, male = 1021 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Herbalist outfit is the perfect outfit for all herbs collectors. Those of you who are aware that you do not necessarily have to reach into the mouth of a hydra to get a hydra tongue and those who know exactly where to get blood- and shadow-herbs will find a matching outfit for their daily hobby. Show the world your affinity for herbs and impress your friends with your knowledge of medicine and potions.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Herder_Male_Addon_3.png", "Outfit_Herder_Female_Addon_3.png" }, - name = "Full Herder Outfit", - price = 750, - sexId = { female = 1280, male = 1279 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Herder is one with nature, being outside all day, watching carefully over his flock. If you like to spend time on picturesque meadows and are always looking for greener pastures, then this outfit is for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Jouster_Male_Addon_3.png", "Outfit_Jouster_Female_Addon_3.png" }, - name = "Full Jouster Outfit", - price = 870, - sexId = { female = 1332, male = 1331 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nThe Jouster is all geared up for a tournament, ready to partake in festive activities involving friendly competition to prove their chivalry. However, being well-armoured, they are also a force to be reckoned with on the battlefield, especially with a trusty steed at their service.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Lupine_Warden_Male_Addon_3.png", "Outfit_Lupine_Warden_Female_Addon_3.png" }, - name = "Full Lupine Warden Outfit", - price = 840, - sexId = { female = 900, male = 899 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you feel the adrenaline rushing through your veins when the sun goes down and a full moon lightens the night? Do you have the urge to hunt down your target no matter what? Unleash the beast inside of you and lead your friends to battle with the Lupine Warden outfit!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Mercenary_Male_Addon_3.png", "Outfit_Mercenary_Female_Addon_3.png" }, - name = "Full Mercenary Outfit", - price = 870, - sexId = { female = 1057, male = 1056 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Mercenary carries a powerful, razor-sharp axe on his shoulders that effortlessly cuts through any armour and bone. You should better tell your friends to keep a safe distance, since heads will roll over the blood-soaked battleground after a powerful swing of yours.\nConsidering the sheer size of this axe, it might even be possible to chop onions without shedding a tear.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Merry_Garb_Male_Addon_3.png", "Outfit_Merry_Garb_Female_Addon_3.png" }, - name = "Full Merry Garb Outfit", - price = 600, - sexId = { female = 1383, male = 1382 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAre you ready for the festive season? Or feeling festive regardless of the time of year? Then the Merry Garb is perfect for you. Donning the outfit not only puts you in a mirthful mood, but spreads blitheness on your travels throughout the lands.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Moth_Cape_Male_Addon_3.png", "Outfit_Moth_Cape_Female_Addon_3.png" }, - name = "Full Moth Cape Outfit", - price = 600, - sexId = { female = 1339, male = 1338 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nIf you are fascinated by this particular group of insects and want to show your deep appreciation of these critters, the Moth Cape is for you. The wing-shaped coat and the antennae provide you with the feeling of being a moth without experiencing the downside of inevitably being drawn to light.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Nordic_Chieftain_Male_Addon_3.png", "Outfit_Nordic_Chieftain_Female_Addon_3.png" }, - name = "Full Nordic Chieftain Outfit", - price = 750, - sexId = { female = 1501, male = 1500 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nWhere others not dare to tread due to the biting cold and freezing winds, the Nordic Chieftain feels right at home. Braving the harsh conditions is possible due to a protective layer of warm clothing, as well as suitable armament to fend off any hostile wildlife. The helmet's massive horns are a tad heavy and unwieldy, but show the chieftain's status.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Owl_Keeper_Male_Addon_3.png", "Outfit_Owl_Keeper_Female_Addon_3.png" }, - name = "Full Owl Keeper Outfit", - price = 600, - sexId = { female = 1174, male = 1173 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nOwl Keepers are often referred to as spirits walking through the forest at night, mere shadows during the day. They are also said to be shamans, protecting the flora and fauna of the Tibian lands. You often see them wearing a stag's antlers on their head and in the company of an owl, for they are as wise and mysterious as these intriguing creatures.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Pharaoh_Male_Addon_3.png", "Outfit_Pharaoh_Female_Addon_3.png" }, - name = "Full Pharaoh Outfit", - price = 750, - sexId = { female = 956, male = 955 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou know how to read hieroglyphs? You admire the exceptional architectural abilities and the unsolved mysteries of an ancient high culture? Next time you pay a visit to your friends, tell them to prepare a bathtub full of milk and honey for you because a Pharaoh is now walking through the streets of Ankrahmun!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Philosopher_Male_Addon_3.png", "Outfit_Philosopher_Female_Addon_3.png" }, - name = "Full Philosopher Outfit", - price = 750, - sexId = { female = 874, male = 873 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you feel the urge to tell people what is really going on in the world? Do you know all answers to the important questions of life? Are you a true philosopher? Then dress like one to showcase the latest fashion for all wise theorists.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Pumpkin_Mummy_Male_Addon_3.png", "Outfit_Pumpkin_Mummy_Female_Addon_3.png" }, - name = "Full Pumpkin Mummy Outfit", - price = 870, - sexId = { female = 1128, male = 1127 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nIf you cannot decide whether to wrap yourself up as a mummy or flaunt an enormous pumpkin head for your next hunting party, why not combine both? The Pumpkin Mummy outfit is the perfect costume for scary nights and spooky days.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Puppeteer_Male_Addon_3.png", "Outfit_Puppeteer_Female_Addon_3.png" }, - name = "Full Puppeteer Outfit", - price = 870, - sexId = { female = 696, male = 697 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAre you a fan of puppetry? You like to travel the world together with one or two little acting fellows? Or are you simply the one who likes to pull the strings? Then the Puppeteer outfit is the right choice for you.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Ranger_Male_Addon_3.png", "Outfit_Ranger_Female_Addon_3.png" }, - name = "Full Ranger Outfit", - price = 750, - sexId = { female = 683, male = 684 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nMost of the day, the Ranger is looking over his forest. He is taking care of all animals and plants and tries to keep everything in balance. Intruders are greeted by a warning shot from his deadly longbow. It is the perfect outfit for Paladins who live in close touch with nature.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Royal_Pumpkin_Male_Addon_3.png", "Outfit_Royal_Pumpkin_Female_Addon_3.png" }, - name = "Full Royal Pumpkin Outfit", - price = 840, - sexId = { male = 760, female = 759 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe mutated pumpkin is too weak for your mighty weapons? Time to show that evil vegetable how to scare the living daylight out of people! Put on a scary looking pumpkin on your head and spread terror and fear amongst the Tibian population.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Rune_Master_Male_Addon_3.png", "Outfit_Rune_Master_Female_Addon_3.png" }, - name = "Full Rune Master Outfit", - price = 870, - sexId = { female = 1385, male = 1384 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Rune Master has dedicated their whole life to the study and mastery of runes. They are intrigued by the ancient symbols, shrouded in mystery, and how their magic works. Rune Masters have a deep understanding of the awesome power they are wielding and can make use of the full potential of runes.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sea_Dog_Male_Addon_3.png", "Outfit_Sea_Dog_Female_Addon_3.png" }, - name = "Full Sea Dog Outfit", - price = 600, - sexId = { female = 749, male = 750 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAhoy mateys! Flaunt the swashbuckling Sea Dog outfit and strike a pose with your hook to impress both landlubbers and fellow pirates. Board your next ship in style!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Seaweaver_Male_Addon_3.png", "Outfit_Seaweaver_Female_Addon_3.png" }, - name = "Full Seaweaver Outfit", - price = 570, - sexId = { female = 732, male = 733 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Seaweaver outfit is the perfect choice if you want to show the world that you are indeed a son or a daughter of the submarine kingdom. You can almost feel the salty taste and the rough wind of the sea when wearing it.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Shadowlotus_Disciple_Male_Addon_3.png", "Outfit_Shadowlotus_Disciple_Female_Addon_3.png" }, - name = "Full Shadowlotus Disciple Outfit", - price = 600, - sexId = { female = 1582, male = 1581 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe path of the Shadowlotus Disciple is a lonesome and threatening one. Only those who forget their name will learn to sneak within shadows. And only those who will overcome their greatest fear can become a true master of assassination.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Siege_Master_Male_Addon_3.png", "Outfit_Siege_Master_Female_Addon_3.png" }, - name = "Full Siege Master Outfit", - price = 600, - sexId = { female = 1050, male = 1051 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nNeither thick stone walls nor heavily armoured gates can stop the Siege Master, who brings down hostile fortifications in the blink of an eye. Whenever he tenses his muscular arms to lift the powerful battering ram, his enemies' knees begin to buckle. It is the perfect outfit for those who also stand for brute strength and immense destruction.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sinister_Archer_Male_Addon_3.png", "Outfit_Sinister_Archer_Female_Addon_3.png" }, - name = "Full Sinister Archer Outfit", - price = 600, - sexId = { female = 1103, male = 1102 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nFrom an early age, the Sinister Archer has been fascinated by people's dark machinations and perversions. Sinister Archers claim that they advocate the good and that they only use their arrows to pierce the hearts of those who have committed many crimes and misdeeds. However, they are still viewed by the public with much suspicion due to their dubious appearance. To keep their identity secret, they often hide themselves behind a skull-like face guard that can easily withstand even axe and club blows.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Spirit_Caller_Male_Addon_3.png", "Outfit_Spirit_Caller_Female_Addon_3.png" }, - name = "Full Spirit Caller Outfit", - price = 600, - sexId = { female = 698, male = 699 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou are in love with the deep soul of Mother Earth and prefer to walk in the shadows of her wooden children? Choose the Spirit Caller outfit to live in harmony with nature.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Sun_Priest_Male_Addon_3.png", "Outfit_Sun_Priest_Female_Addon_3.png" }, - name = "Full Sun Priest Outfit", - price = 750, - sexId = { female = 1024, male = 1023 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nDo you worship warm temperatures and are opposed to the thought of long and dark winter nights? Do you refuse to spend countless evenings in front of your chimney while ice-cold wind whistles through the cracks and niches of your house? It is time to stop freezing and to become an honourable Sun Priest! With this stylish outfit, you can finally show the world your unconditional dedication and commitment to the sun!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Trailblazer_Male_Addon_3.png", "Outfit_Trailblazer_Female_Addon_3.png" }, - name = "Full Trailblazer Outfit", - price = 600, - sexId = { female = 1293, male = 1292 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nThe Trailblazer is on a mission of enlightenment and carries the flame of wisdom near and far. The everlasting shine brightens the hearts and minds of all creatures its rays touch, bringing light even to the darkest corners of the world as a beacon of insight and knowledge.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Trophy_Hunter_Male_Addon_3.png", "Outfit_Trophy_Hunter_Female_Addon_3.png" }, - name = "Full Trophy Hunter Outfit", - price = 870, - sexId = { female = 900, male = 899 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nYou spend hours in the woods in search of wild and rare animals? Countless stuffed skulls of deer, wolves and other creatures are decorating your walls? Now you have the chance to present your trophies in public. Become a Trophy Hunter and cover your shoulders with the finest bear skulls!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Veteran_Paladin_Male_Addon_3.png", "Outfit_Veteran_Paladin_Female_Addon_3.png" }, - name = "Full Veteran Paladin Outfit", - price = 750, - sexId = { female = 1205, male = 1204 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nA Veteran Paladin has mastered the art of distance fighting. No matter how far away his prey may be, a marksman like the Veteran Paladin will always hit with extraordinary precision. No one can escape his keen hawk-eyed vision and even small stones become deadly weapons in his hands.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Void_Master_Male_Addon_3.png", "Outfit_Void_Master_Female_Addon_3.png" }, - name = "Full Void Master Outfit", - price = 750, - sexId = { female = 1203, male = 1202 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nAccording to ancient rumours, the pulsating orb that the Void Master balances skilfully on the tip of his staff consists of powerful cosmic spheres. If you gaze too long into the infinite emptiness inside the orb, its powers will absorb your mind.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Winter_Warden_Male_Addon_3.png", "Outfit_Winter_Warden_Female_Addon_3.png" }, - name = "Full Winter Warden Outfit", - price = 870, - sexId = { female = 852, male = 853 }, - addon = 3, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe warm and cosy cloak of the Winter Warden outfit will keep you warm in every situation. Best thing, it is not only comfortable but fashionable as well. You will be the envy of any snow queen or king, guaranteed!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Citizen_Male.png", "Outfit_Retro_Citizen_Female.png" }, - name = "Retro Citizen", - price = 870, - sexId = { female = 975, male = 974 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nDo you still remember your first stroll through the streets of Thais? For old times' sake, walk the paths of Nostalgia as a Retro Citizen!", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Hunter_Male.png", "Outfit_Retro_Hunter_Female.png" }, - name = "Retro Hunter", - price = 870, - sexId = { female = 973, male = 972 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWhenever you pick up your bow and spears, you walk down memory lane and think of your early days? Treat yourself with the fashionable Retro Hunter outfit and hunt some good old monsters from your childhood.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Knight_Male.png", "Outfit_Retro_Knight_Female.png" }, - name = "Retro Knight", - price = 870, - sexId = { female = 971, male = 970 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWho needs a fancy looking sword with bling-bling and ornaments? Back in the days, we survived without such unnecessary accessories! Time to show those younkers what a Retro Knight is made of.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Mage_Male.png", "Outfit_Retro_Mage_Female.png" }, - name = "Retro Mage", - price = 870, - sexId = { female = 969, male = 968 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nDress up as a Retro Mage and you will always cut a fine figure on the battleground while eliminating your enemies with your magical powers the old-fashioned way.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Nobleman_Male.png", "Outfit_Retro_Nobleman_Female.png" }, - name = "Retro Noble(wo)man", - price = 870, - sexId = { female = 967, male = 966 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nKing Tibianus has invited you to a summer ball and you have nothing to wear for this special event? Do not worry, the Retro Noble(wo)man outfit makes you a real eye catcher on every festive occasion.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Summoner_Male.png", "Outfit_Retro_Summoner_Female.png" }, - name = "Retro Summoner", - price = 870, - sexId = { female = 965, male = 964 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nWhile the Retro Mage usually throws runes and mighty spells directly at the enemies, the Retro Summoner outfit might be the better choice for Tibians that prefer to send mighty summons to the battlefield to keep their enemies at distance.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Outfit_Retro_Warrior_Male.png", "Outfit_Retro_Warrior_Female.png" }, - name = "Retro Warrior", - price = 870, - sexId = { female = 963, male = 962 }, - description = "{character}\n{info} colours can be changed using the Outfit dialog\n\nYou are fearless and strong as a behemoth but have problems finding the right outfit for your adventures? The Retro Warrior outfit is a must-have for all fashion-conscious old-school Tibians out there.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - }, - }, - -- House - { - icons = { "Category_HouseTools.png" }, - name = "Houses", - rookgaard = true, - subclasses = { "Decorations", "Furniture", "Upgrades", "Hirelings", "Hirelings Dresses" }, - }, - -- House ~ Decorations - { - icons = { "Category_HouseDecorations.png" }, - name = "Decorations", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Alchemistic_Bookstand.png" }, - name = "Alchemistic Bookstand", - price = 100, - itemtype = 27679, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Cupboard.png" }, - name = "Alchemistic Cupboard", - price = 50, - itemtype = 27689, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Scales.png" }, - name = "Alchemistic Scales", - price = 120, - itemtype = 27683, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "All-Seeing_Tapestry.png" }, - name = "All-Seeing Tapestry", - price = 60, - itemtype = 23450, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Anglerfish_Lamp.png" }, - name = "Anglerfish Lamp", - price = 120, - itemtype = 28675, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Anvil.png" }, - name = "Anvil", - price = 120, - itemtype = 35185, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Arrival_at_Thais_Painting.png" }, - name = "Arrival The Thais Paint", - price = 50, - itemtype = 27698, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Azure_Carpet.png" }, - name = "Azure Carpet", - price = 35, - itemtype = 23710, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Bonelord.png" }, - name = "Baby Bonelord", - price = 250, - itemtype = 34026, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Dragon.png" }, - name = "Baby Dragon", - price = 250, - itemtype = 23442, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Elephant.png" }, - name = "Baby Elephant", - price = 250, - itemtype = 35153, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Polar_Bear.png" }, - name = "Baby Polar Bear", - price = 250, - itemtype = 32790, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Rotworm.png" }, - name = "Baby Rotworm", - price = 150, - itemtype = 28690, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Seal.png" }, - name = "Baby Seal", - price = 250, - itemtype = 32788, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Unicorn.png" }, - name = "Baby Unicorn", - price = 250, - itemtype = 31703, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Bamboo_Mat.png" }, - name = "Bamboo Mat", - price = 25, - itemtype = 23433, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Barrel.png" }, - name = "Barrel", - price = 60, - itemtype = 34300, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Barrel_&_Anchor_Lamp.png" }, - name = "Barrel & Anchor Lamp", - price = 80, - itemtype = 31937, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bat.png" }, - name = "Bat", - price = 180, - itemtype = 33040, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bath_Tub.png" }, - name = "Bath Tub", - price = 250, - itemtype = 26076, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bellflower.png" }, - name = "Bellflower", - price = 50, - itemtype = 28697, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Bitter-Smack_Leaf.png" }, - name = "Bitter-Smack Leaf", - price = 50, - itemtype = 25217, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blank_Zaoan_Panel.png" }, - name = "Blank Zaoan Panel", - price = 50, - itemtype = 37777, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blooming_Cactus.png" }, - name = "Blooming Cactus", - price = 50, - itemtype = 25216, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blue_Round_Cushion.png" }, - name = "Blue Round Cushion", - price = 50, - itemtype = 31222, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Blue_Square_Cushion.png" }, - name = "Blue Square Cushion", - price = 50, - itemtype = 31219, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Brocade_Tapestry.png" }, - name = "Brocade Tapestry", - price = 50, - itemtype = 23725, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Captain_Crab.png" }, - name = "Captain Crab", - price = 180, - itemtype = 42308, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carnivorous_Plant.png" }, - name = "Carnivorous Plant", - price = 50, - itemtype = 28689, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cat_in_a_Basket.png" }, - name = "Cat in a Basket", - price = 150, - itemtype = 23451, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Chameleon.png" }, - name = "Chameleon", - price = 250, - itemtype = 25213, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Chest_of_Abundance.png" }, - name = "Chest of Abundance", - price = 120, - itemtype = 28945, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Colourful_Carpet.png" }, - name = "Colourful Carpet", - price = 35, - itemtype = 24417, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Colourful_PomPom_Carpet.png" }, - name = "Colourful Pom-Pom Carpet", - price = 30, - itemtype = 35889, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Crested_Carpet.png" }, - name = "Crested Carpet", - price = 25, - itemtype = 26152, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Crimson_Carpet.png" }, - name = "Crimson Carpet", - price = 35, - itemtype = 23707, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Crystal_Lamp.png" }, - name = "Crystal Lamp", - price = 80, - itemtype = 31196, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Curly_Hortensis_Lamp.png" }, - name = "Curly Hortensis Lamp", - price = 120, - itemtype = 31695, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dark_Parquet.png" }, - name = "Dark Parquet", - price = 30, - itemtype = 23713, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Decorated_Carpet.png" }, - name = "Decorated Carpet", - price = 35, - itemtype = 26154, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Baller.png" }, - name = "Demon Baller", - price = 250, - itemtype = 36646, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Pet.png" }, - name = "Demon Pet", - price = 250, - itemtype = 26173, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Skull.png" }, - name = "Demon Skull", - price = 50, - itemtype = 31212, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Statue.png" }, - name = "Demon Statue", - price = 25, - itemtype = 34058, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Diamond_Carpet.png" }, - name = "Diamond Carpet", - price = 25, - itemtype = 24420, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Djinn_Lamp.png" }, - name = "Djinn Lamp", - price = 180, - itemtype = 42363, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dog_House.png" }, - name = "Dog House", - price = 150, - itemtype = 23697, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dragon_Plant.png" }, - name = "Dragon Plant", - price = 180, - itemtype = 37021, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Drawing_Board.png" }, - name = "Drawing Board", - price = 100, - itemtype = 34062, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dungeon_Scene_Painting.png" }, - name = "Dungeon Scene Painting", - price = 100, - itemtype = 27697, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Emerald_Carpet.png" }, - name = "Emerald Carpet", - price = 35, - itemtype = 23711, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fennec.png" }, - name = "Fennec", - price = 150, - itemtype = 28694, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} to trigger an animation feed it with meat, ham, dragon ham, haunch of a boar, roasted meat or bug meat\n{info} can be fed once every 65 seconds\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Bust.png" }, - name = "Ferumbras Dust", - price = 70, - itemtype = 27692, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}\n{useicon} house owner can use it to display a duplicate of an owned Ferumbras' Hat on this bust - also works if the character has already turned Ferumbras' hat in to earn the outfit addon", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Portrait.png" }, - name = "Ferumbras Portrait", - price = 100, - itemtype = 27700, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Snowman.png" }, - name = "Ferumbras Snowman", - price = 100, - itemtype = 32786, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Filled_Shoes.png" }, - name = "Festive Filled Shoes", - price = 50, - itemtype = 30227, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Fireplace.png" }, - name = "Festive Fireplace", - price = 180, - itemtype = 30233, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Pile_of_Presents.png" }, - name = "Festive Pile of Presents", - price = 50, - itemtype = 30245, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Pyramid.png" }, - name = "Festive Pyramid", - price = 120, - itemtype = 30248, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Rocking_Chair.png" }, - name = "Festive Rocking Chair", - price = 50, - itemtype = 30241, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Sack_of_Presents.png" }, - name = "Festive Sack of Presents", - price = 50, - itemtype = 30247, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Sleigh.png" }, - name = "Festive Sleigh", - price = 50, - itemtype = 30244, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Table.png" }, - name = "Festive Table", - price = 100, - itemtype = 30229, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Festive_Tree.png" }, - name = "Festive Tree", - price = 180, - itemtype = 30237, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fish_Hook_Board.png" }, - name = "Fish Hook Board", - price = 50, - itemtype = 42304, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fish_in_a_Tank.png" }, - name = "Fish Tank", - price = 180, - itemtype = 23691, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Flowery_Carpet.png" }, - name = "Flowery Carpet", - price = 35, - itemtype = 24416, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Flowery_Grass.png" }, - name = "Flowery Grass", - price = 30, - itemtype = 39797, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Fluorescent_Fungi.png" }, - name = "Fluorescent Fungi", - price = 60, - itemtype = 28920, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Forge.png" }, - name = "Forge", - price = 120, - itemtype = 35155, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Forget-Me-Not.png" }, - name = "Forget-Me-Not", - price = 50, - itemtype = 28698, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Four_Hearts_Lamp.png" }, - name = "Four Hearts Lamp", - price = 120, - itemtype = 33028, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Fur_Carpet.png" }, - name = "Fur Carpet", - price = 30, - itemtype = 24419, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Gloomy_Poisonous_Fungi.png" }, - name = "Gloomy Poisonous Fungi", - price = 60, - itemtype = 28926, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Glowing_Sulphur_Fungi.png" }, - name = "Glowing Sulphur Fungi", - price = 60, - itemtype = 28924, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Glowworms.png" }, - name = "Glowworms", - price = 180, - itemtype = 34270, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Golden_Demon_Skull.png" }, - name = "Golden Demon Skull", - price = 80, - itemtype = 31211, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Golden_Dragon_Tapestry.png" }, - name = "Golden Dragon Tapestry", - price = 70, - itemtype = 23723, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{info} drag the unwrapped tapestry to a wall to hang it up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Golden_Minotaur_Skull.png" }, - name = "Golden Minotaur Skull", - price = 100, - itemtype = 31209, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Carpet.png" }, - name = "Grandiose Carpet", - price = 35, - itemtype = 35942, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Lamp.png" }, - name = "Grandiose Lamp", - price = 80, - itemtype = 35943, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Painting.png" }, - name = "Grandiose Painting", - price = 50, - itemtype = 35940, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grass.png" }, - name = "Grass", - price = 30, - itemtype = 37019, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Green_Round_Cushion.png" }, - name = "Green Round Cushion", - price = 50, - itemtype = 31221, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Green_Square_Cushion.png" }, - name = "Green Square Cushion", - price = 50, - itemtype = 31218, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grinding_Wheel.png" }, - name = "Grinding Wheel", - price = 80, - itemtype = 35177, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hamster_in_a_Wheel.png" }, - name = "Hamster in a Wheel", - price = 180, - itemtype = 23444, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Heart_Lamp.png" }, - name = "Heart Lamp", - price = 180, - itemtype = 33026, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Hedgehog.png" }, - name = "Hedgehog", - price = 150, - itemtype = 31680, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Weapons_Rack.png" }, - name = "Hrodmir Weapons Rack", - price = 90, - itemtype = 26081, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ice_Chandelier.png" }, - name = "Ice_Chandelier", - price = 180, - itemtype = 32784, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Idol_Lamp.png" }, - name = "Idol Lamp", - price = 80, - itemtype = 31214, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Incomprehensible_Riches.png" }, - name = "Incomprehensible Riches", - price = 90, - itemtype = 28944, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "King_Tibianus_Bust.png" }, - name = "King Tibianus Bust", - price = 50, - itemtype = 27702, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Clock.png" }, - name = "Kitchen Clock", - price = 80, - itemtype = 34309, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Lamp.png" }, - name = "Kitchen Lamp", - price = 80, - itemtype = 34304, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Shelf.png" }, - name = "Kitchen Shelf", - price = 80, - itemtype = 34282, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Candelabra.png" }, - name = "Knightly Candelabra", - price = 60, - itemtype = 39498, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Candle_Holder.png" }, - name = "Knightly Candle Holder", - price = 60, - itemtype = 39500, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Fire_Bowl.png" }, - name = "Knightly Fire Bowl", - price = 80, - itemtype = 39443, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Guard.png" }, - name = "Knightly Guard", - price = 250, - itemtype = 39508, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Sword_Lamp.png" }, - name = "Knightly Sword Lamp", - price = 60, - itemtype = 39496, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Wall_Lamp.png" }, - name = "Knightly Wall Lamp", - price = 60, - itemtype = 39446, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Buoy_Lamp.png" }, - name = "Kraken Buoy Lamp", - price = 60, - itemtype = 37187, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Shelf.png" }, - name = "Kraken Shelf", - price = 100, - itemtype = 37189, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Tentacle_Lamp.png" }, - name = "Kraken Tentacle Lamp", - price = 60, - itemtype = 37520, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Watcher_Lamp.png" }, - name = "Kraken Watcher Lamp", - price = 80, - itemtype = 37700, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Life_Buoy.png" }, - name = "Life Buoy", - price = 50, - itemtype = 42305, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Light_of_Change.png" }, - name = "Light of Change", - price = 180, - itemtype = 27667, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Light_Parquet.png" }, - name = "Light Parquet", - price = 30, - itemtype = 23712, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Predator_Lamp.png" }, - name = "Lit Predator Lamp", - price = 60, - itemtype = 23436, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Protectress_Lamp.png" }, - name = "Lit Protectress Lamp", - price = 90, - itemtype = 23440, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lit_Skull_Lamp.png" }, - name = "Lit Skull Lamp", - price = 90, - itemtype = 24435, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Little_Big_Flower_Lamp.png" }, - name = "Little Big Flower Lamp", - price = 80, - itemtype = 31697, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Loose_Opulent_Floor_Intarsia.png" }, - name = "Loose Opulent Floor Intarsia", - price = 30, - itemtype = 42338, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Lordly_Tapestry.png" }, - name = "Lordly Tapestry", - price = 50, - itemtype = 23448, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Luminescent_Fungi.png" }, - name = "Luminescent Fungi", - price = 60, - itemtype = 28922, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Marble_Floor.png" }, - name = "Marble Floor", - price = 30, - itemtype = 23720, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Menacing_Tapestry.png" }, - name = "Menacing Tapestry", - price = 70, - itemtype = 23449, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Merchant_Portrait.png" }, - name = "Merchant Portrait", - price = 100, - itemtype = 42343, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mermaid_Figure_Head.png" }, - name = "Mermaid Figure Head", - price = 120, - itemtype = 23449, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Metal_Wall_Lamp.png" }, - name = "Metal Wall Lamp", - price = 80, - itemtype = 35161, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Midnight_Panther_Rug.png" }, - name = "Midnight Panther Rug", - price = 30, - itemtype = 35895, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Minotaur_Skull.png" }, - name = "Minotaur Skull", - price = 70, - itemtype = 31210, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Model_Ship_Lamp.png" }, - name = "Model Ship Lamp", - price = 80, - itemtype = 31942, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Monkey.png" }, - name = "Monkey", - price = 180, - itemtype = 31955, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Moon_Carpet.png" }, - name = "Moon Carpet", - price = 35, - itemtype = 35898, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Mystic_Carpet.png" }, - name = "Mystic Carpet", - price = 35, - itemtype = 26118, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Natural_PomPom_Carpet.png" }, - name = "Natural Pom-Pom Carpet", - price = 30, - itemtype = 35891, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Night_Sky_Carpet.png" }, - name = "Night Sky Carpet", - price = 25, - itemtype = 24422, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Octoputz.png" }, - name = "Octoputz", - price = 180, - itemtype = 37211, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Carpet.png" }, - name = "Opulent Carpet", - price = 30, - itemtype = 42341, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Floor_Intarsia.png" }, - name = "Opulent Floor Intarsia", - price = 30, - itemtype = 42339, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Floor_Lamp.png" }, - name = "Opulent Floor Lamp", - price = 60, - itemtype = 42348, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Wooden_Floor.png" }, - name = "Opulent Wooden Floor", - price = 30, - itemtype = 42337, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Wood_Floor_Planks.png" }, - name = "Opulent Wood Floor Planks", - price = 30, - itemtype = 42336, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Oven.png" }, - name = "Oven", - price = 120, - itemtype = 37272, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Owin_Rug.png" }, - name = "Owin Rug", - price = 30, - itemtype = 35893, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Painting_of_Tibiasula.png" }, - name = "Painting of Tibiasula", - price = 250, - itemtype = 28947, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pair_of_Bellows.png" }, - name = "Pair of Bellows", - price = 250, - itemtype = 35181, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Parrot.png" }, - name = "Parrot", - price = 180, - itemtype = 24432, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-Up_Opulent_Carpet.png" }, - name = "Rolled-Up Opulent Carpet", - price = 30, - itemtype = 42340, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Patterned_Carpet.png" }, - name = "Patterned Carpet", - price = 30, - itemtype = 24421, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pile_of_Alchemistic_Books.png" }, - name = "Pile of Alchemistic Books", - price = 120, - itemtype = 27687, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pile_of_Riches.png" }, - name = "Pile of Riches", - price = 90, - itemtype = 42342, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pink_Roses.png" }, - name = "Pink Roses", - price = 50, - itemtype = 25218, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pink_Shroom_Lamp.png" }, - name = "Pink Shroom Lamp", - price = 80, - itemtype = 37018, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Flag.png" }, - name = "Pirate Flag", - price = 50, - itemtype = 31945, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Ship_Ballista.png" }, - name = "Pirate Ship Ballista", - price = 120, - itemtype = 31933, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Skeleton_Cage.png" }, - name = "Pirate Skeleton Cage", - price = 120, - itemtype = 31947, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Treasure_Chest.png" }, - name = "Pirate Treasure Chest", - price = 120, - itemtype = 31936, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Pirate_Treasure_Map.png" }, - name = "Pirate Treasure Map", - price = 50, - itemtype = 31946, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Podium_of_Renown.png" }, - name = "Podium of Renown", - price = 500, - itemtype = 35973, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Podium_of_Tenacity.png" }, - name = "Podium of Tenacity", - price = 375, - itemtype = 42367, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Portable_Aqueduct.png" }, - name = "Portable Aqueduct", - price = 250, - itemtype = 35949, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Purple_Flower_Lamp.png" }, - name = "Purple Flower Lamp", - price = 80, - itemtype = 39795, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Queen_Eloise_Bust.png" }, - name = "Queen Eloise Bust", - price = 50, - itemtype = 27695, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Red_Geranium.png" }, - name = "Red Geranium", - price = 50, - itemtype = 28699, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Red_Roses.png" }, - name = "Red Roses", - price = 50, - itemtype = 25219, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Romantic_Carpet.png" }, - name = "Romantic Carpet", - price = 30, - itemtype = 35899, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sabertooth_Skull.png" }, - name = "Sabertooth Skull", - price = 100, - itemtype = 34060, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Exalted_Sarcophagus.png" }, - name = "Sarcophagus", - price = 120, - itemtype = 31683, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Scales_Wall_Lamp.png" }, - name = "Scales Wall Lamp", - price = 60, - itemtype = 42345, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculpture_of_a_Fox.png" }, - name = "Sculpture of a Fox", - price = 180, - itemtype = 37811, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculpture_of_a_Noblewoman.png" }, - name = "Sculpture of a Noblewoman", - price = 120, - itemtype = 34064, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculpture_of_an_Ocotoputz.png" }, - name = "Sculpture of an Octoputz", - price = 120, - itemtype = 37205, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sea-devil_Wall_Lamp.png" }, - name = "Sea-devil Wall Lamp", - price = 60, - itemtype = 42300, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seafood_Bucket.png" }, - name = "Seafood Bucket", - price = 60, - itemtype = 42301, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seashell_Lamp.png" }, - name = "Seashell Lamp", - price = 80, - itemtype = 42292, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Shaggy_Carpet.png" }, - name = "Shaggy Carpet", - price = 30, - itemtype = 26116, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ship_Bell.png" }, - name = "Ship Bell", - price = 50, - itemtype = 42303, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ship's_Wheel.png" }, - name = "Ship's Wheel", - price = 50, - itemtype = 31948, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Small_Hearts_Lamp.png" }, - name = "Small Hearts Lamp", - price = 90, - itemtype = 31948, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Star_Carpet.png" }, - name = "Star Carpet", - price = 25, - itemtype = 24423, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Piled-up_Stone_Tiles.png" }, - name = "Stone Tiles", - price = 25, - itemtype = 26121, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Striped_Carpet .png" }, - name = "Striped Carpet", - price = 30, - itemtype = 24418, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Stuffed_Bear_Display.png" }, - name = "Stuffed Bear Display", - price = 90, - itemtype = 28928, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Stuffed_Teddy_Display.png" }, - name = "Stuffed Teddy Display", - price = 50, - itemtype = 28930, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sulphur_Blossom_Lamp.png" }, - name = "Sulphur Blossom Lamp", - price = 80, - itemtype = 31723, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sword_Tapestry.png" }, - name = "Sword Tapestry", - price = 60, - itemtype = 23724, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tendrils.png" }, - name = "Tendrils", - price = 50, - itemtype = 39803, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tentacle_Lamp.png" }, - name = "Tentacle Lamp", - price = 80, - itemtype = 42298, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Terrarium_Snake.png" }, - name = "Terrarium Snake", - price = 180, - itemtype = 26171, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Spider_in_a_Terrarium.png" }, - name = "Terrarium Spider", - price = 180, - itemtype = 26078, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tibia_Streets_Painting.png" }, - name = "Tibia Streets Painting", - price = 100, - itemtype = 27699, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Torch_of_Change.png" }, - name = "Torch of Change", - price = 120, - itemtype = 27673, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Turquoise_Flower_Lamp.png" }, - name = "Turquoise Flower Lamp", - price = 60, - itemtype = 39793, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vegetable_Basket.png" }, - name = "Vegetable Basket", - price = 50, - itemtype = 34302, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Lamp.png" }, - name = "Vengothic Lamp", - price = 180, - itemtype = 25210, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Venorean_Table_Clock.png" }, - name = "Venorean Table Clock", - price = 120, - itemtype = 26112, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Verdant_Carpet.png" }, - name = "Verdant Carpet", - price = 30, - itemtype = 26114, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Violet_Round_Cushion.png" }, - name = "Violet Round Cushion", - price = 50, - itemtype = 31220, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Violet_Square_Cushion.png" }, - name = "Violet Square Cushion", - price = 50, - itemtype = 31217, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Basin.png" }, - name = "Volcanic Basin", - price = 90, - itemtype = 36618, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Bulb.png" }, - name = "Volcanic Bulb", - price = 80, - itemtype = 36624, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Mirror.png" }, - name = "Volcanic Mirror", - price = 120, - itemtype = 36626, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Sphere.png" }, - name = "Volcanic Sphere", - price = 90, - itemtype = 36620, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Spire.png" }, - name = "Volcanic Spire", - price = 80, - itemtype = 36620, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wall_Fern.png" }, - name = "Wall Fern", - price = 50, - itemtype = 39800, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wall_Flowers.png" }, - name = "Wall Flowers", - price = 50, - itemtype = 39799, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wall_Leaves.png" }, - name = "Wall Leaves", - price = 50, - itemtype = 39801, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wallcupboard.png" }, - name = "Wallcupboard", - price = 50, - itemtype = 34280, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Water_Bucket.png" }, - name = "Water Bucket", - price = 60, - itemtype = 34280, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Water_Nymph.png" }, - name = "Water Nymph", - price = 180, - itemtype = 39805, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Wheat_Carpet.png" }, - name = "Wheat Carpet", - price = 30, - itemtype = 26151, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_White_Fur_Carpet.png" }, - name = "White Fur Carpet", - price = 30, - itemtype = 23432, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "White_Shark_Trophy.png" }, - name = "White Shark Trophy", - price = 80, - itemtype = 31951, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Wooden_Planks.png" }, - name = "Wooden Planks", - price = 25, - itemtype = 26123, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wooden_Sandals.png" }, - name = "Wooden Sandals", - price = 40, - itemtype = 37801, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rolled-up_Yalaharian_Carpet.png" }, - name = "Yalaharian Carpet", - price = 35, - itemtype = 23431, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Yellow_Roses.png" }, - name = "Yellow Roses", - price = 50, - itemtype = 25220, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Yellow_Shroom_Lamp.png" }, - name = "Yellow Shroom Lamp", - price = 60, - itemtype = 37015, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_1.png" }, - name = "Zaoan Bamboo Tiles 1", - price = 30, - itemtype = 37763, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_2.png" }, - name = "Zaoan Bamboo Tiles 2", - price = 30, - itemtype = 37764, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_3.png" }, - name = "Zaoan Bamboo Tiles 3", - price = 30, - itemtype = 37765, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_4.png" }, - name = "Zaoan Bamboo Tiles 4", - price = 30, - itemtype = 37766, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_5.png" }, - name = "Zaoan Bamboo Tiles 5", - price = 30, - itemtype = 37767, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bamboo_Tiles_6.png" }, - name = "Zaoan Bamboo Tiles 6", - price = 30, - itemtype = 37768, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Bonsai.png" }, - name = "Zaoan Bonsai", - price = 50, - itemtype = 37798, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Divider.png" }, - name = "Zaoan Divider", - price = 60, - itemtype = 37815, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Drawing.png" }, - name = "Zaoan Drawing", - price = 50, - itemtype = 37800, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Panel.png" }, - name = "Zaoan Panel", - price = 50, - itemtype = 37776, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Panel_Base.png" }, - name = "Zaoan Panel Base", - price = 50, - itemtype = 37775, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Paravent.png" }, - name = "Zaoan Paravent", - price = 60, - itemtype = 37784, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Pot_Bamboo.png" }, - name = "Zaoan Pot Bamboo", - price = 50, - itemtype = 37799, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Wall_Lamp.png" }, - name = "Zaoan Wall Lamp", - price = 60, - itemtype = 37806, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Wall_Lamps.png" }, - name = "Zaoan Wall Lamps", - price = 60, - itemtype = 37808, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- House ~ Furniture - { - icons = { "Category_HouseFurniture.png" }, - name = "Furniture", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Alchemistic_Cabinet.png" }, - name = "Alchemistic Cabinet", - price = 100, - itemtype = 27664, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Chair.png" }, - name = "Alchemistic Chair", - price = 50, - itemtype = 27662, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Alchemistic_Table.png" }, - name = "Alchemistic Table", - price = 80, - itemtype = 27665, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Artist_Chair.png" }, - name = "Artist Chair", - price = 50, - itemtype = 34036, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Artist_Chest.png" }, - name = "Artist Chest", - price = 50, - itemtype = 34040, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Artist_Shelf.png" }, - name = "Artist Shelf", - price = 110, - itemtype = 34030, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Artist_Table.png" }, - name = "Artist Table", - price = 80, - itemtype = 34034, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Cabinet.png" }, - name = "Comfy Cabinet", - price = 100, - itemtype = 28942, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Chair.png" }, - name = "Comfy Chair", - price = 70, - itemtype = 28934, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Chest.png" }, - name = "Comfy Chest", - price = 60, - itemtype = 28938, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Comfy_Table.png" }, - name = "Comfy Table", - price = 60, - itemtype = 28936, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cupboard.png" }, - name = "Cupboard", - price = 90, - itemtype = 34276, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Cabinet.png" }, - name = "Dwarven Stone Cabinet", - price = 100, - itemtype = 31192, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Chair.png" }, - name = "Dwarven Stone Chair", - price = 50, - itemtype = 31185, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Chest.png" }, - name = "Dwarven Stone Chest", - price = 80, - itemtype = 31187, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Dwarven_Stone_Table.png" }, - name = "Dwarven Stone Table", - price = 50, - itemtype = 31191, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use it to open up some storage space\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Cabinet.png" }, - name = "Ferocious Cabinet", - price = 110, - itemtype = 23421, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Chair.png" }, - name = "Ferocious Chair", - price = 50, - itemtype = 23409, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Table.png" }, - name = "Ferocious Table", - price = 50, - itemtype = 23414, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferocious_Trunk.png" }, - name = "Ferocious Trunk", - price = 80, - itemtype = 23423, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Flower_Cabinet.png" }, - name = "Flower Cabinet", - price = 90, - itemtype = 39775, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Flower_Chair.png" }, - name = "Flower Chair", - price = 60, - itemtype = 39768, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Flower_Chest.png" }, - name = "Flower Chest", - price = 60, - itemtype = 39777, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Flower_Table.png" }, - name = "Flower Table", - price = 80, - itemtype = 39777, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Chair.png" }, - name = "Grandiose Chair", - price = 60, - itemtype = 35915, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Couch_Left.png" }, - name = "Grandiose Couch Left", - price = 60, - itemtype = 35959, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Couch_Middle.png" }, - name = "Grandiose Couch Middle", - price = 60, - itemtype = 35960, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Couch_Right.png" }, - name = "Grandiose Couch Right", - price = 60, - itemtype = 35961, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Cupboard.png" }, - name = "Grandiose Cupboard", - price = 100, - itemtype = 35911, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Gilded_Chest.png" }, - name = "Grandiose Gilded Chest", - price = 90, - itemtype = 35923, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Refined_Chest.png" }, - name = "Grandiose Refined Chest", - price = 70, - itemtype = 35919, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Grandiose_Table.png" }, - name = "Grandiose Table", - price = 50, - itemtype = 35913, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Heart_Cabinet.png" }, - name = "Heart Cabinet", - price = 100, - itemtype = 33032, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Heart_Chair.png" }, - name = "Heart Chair", - price = 50, - itemtype = 33036, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Heart_Chest.png" }, - name = "Heart Chest", - price = 80, - itemtype = 33043, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Heart_Table.png" }, - name = "Heart Table", - price = 80, - itemtype = 33043, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Chair.png" }, - name = "Hrodmir Chair", - price = 50, - itemtype = 31693, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Chest.png" }, - name = "Hrodmir Chest", - price = 80, - itemtype = 31687, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Cupboard.png" }, - name = "Hrodmir Cupboard", - price = 100, - itemtype = 31705, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Hrodmir_Table.png" }, - name = "Hrodmir Table", - price = 50, - itemtype = 31679, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ice_Cabinet.png" }, - name = "Ice Cabinet", - price = 100, - itemtype = 32775, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ice_Chest.png" }, - name = "Ice Chest", - price = 80, - itemtype = 32780, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ice_Stool.png" }, - name = "Ice Stool", - price = 50, - itemtype = 32778, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ice_Table.png" }, - name = "Ice Table", - price = 60, - itemtype = 32777, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Katana_Display.png" }, - name = "Katana Display", - price = 70, - itemtype = 37804, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Chair.png" }, - name = "Kitchen Chair", - price = 50, - itemtype = 34292, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Chest.png" }, - name = "Kitchen Chest", - price = 50, - itemtype = 34296, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kitchen_Table.png" }, - name = "Kitchen Table", - price = 100, - itemtype = 34285, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Bench_Large_Left.png" }, - name = "Knightly Bench Large Left", - price = 80, - itemtype = 39518, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Bench_Large_Right.png" }, - name = "Knightly Bench Large Right", - price = 80, - itemtype = 39520, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Bench_Left.png" }, - name = "Knightly Bench Left", - price = 60, - itemtype = 39517, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Bench_Middle.png" }, - name = "Knightly Bench Middle", - price = 80, - itemtype = 39519, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Bench_Right.png" }, - name = "Knightly Bench Right", - price = 60, - itemtype = 39521, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Cabinet.png" }, - name = "Knightly Cabinet", - price = 100, - itemtype = 39441, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Chair.png" }, - name = "Knightly Chair", - price = 70, - itemtype = 39419, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Chess_Table.png" }, - name = "Knightly Chess Table", - price = 60, - itemtype = 39427, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Chest.png" }, - name = "Knightly Chest", - price = 60, - itemtype = 39504, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Decorative_Shield.png" }, - name = "Knightly Decorative Shield", - price = 60, - itemtype = 39502, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Knightly_Table.png" }, - name = "Knightly Table", - price = 60, - itemtype = 39423, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Cabinet.png" }, - name = "Kraken Cabinet", - price = 100, - itemtype = 37179, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Chair.png" }, - name = "Kraken Chair", - price = 60, - itemtype = 37174, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Chest.png" }, - name = "Kraken Chest", - price = 70, - itemtype = 37181, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Kraken_Table.png" }, - name = "Kraken Table", - price = 60, - itemtype = 37178, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Leaf_Chair.png" }, - name = "Leaf Chair", - price = 80, - itemtype = 37004, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Log Chest.png" }, - name = "Log Chest", - price = 80, - itemtype = 37011, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Cabinet.png" }, - name = "Magnificent Cabinet", - price = 100, - itemtype = 23419, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Chair.png" }, - name = "Magnificent Chair", - price = 60, - itemtype = 23405, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Table.png" }, - name = "Magnificent Table", - price = 60, - itemtype = 23418, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Magnificent_Trunk.png" }, - name = "Magnificent Trunk", - price = 70, - itemtype = 23427, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Book_Case.png" }, - name = "Opulent Book Case", - price = 100, - itemtype = 42332, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Chair.png" }, - name = "Opulent Chair", - price = 60, - itemtype = 42320, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Chest.png" }, - name = "Opulent Chest", - price = 60, - itemtype = 42328, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Item_Stand.png" }, - name = "Opulent Item Stand", - price = 50, - itemtype = 42350, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Spice_Rack.png" }, - name = "Opulent Spice Rack", - price = 100, - itemtype = 42334, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Opulent_Table.png" }, - name = "Opulent Table", - price = 70, - itemtype = 42324, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Cabinet.png" }, - name = "Ornate Cabinet", - price = 100, - itemtype = 26162, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Chair.png" }, - name = "Ornate Chair", - price = 50, - itemtype = 26158, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Chest.png" }, - name = "Ornate Chest", - price = 80, - itemtype = 26165, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Table.png" }, - name = "Ornate Table", - price = 50, - itemtype = 26161, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Round_Side_Table.png" }, - name = "Round Side Table", - price = 50, - itemtype = 31208, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Cabinet.png" }, - name = "Rustic Cabinet", - price = 100, - itemtype = 23700, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Chair.png" }, - name = "Rustic Chair", - price = 50, - itemtype = 23695, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Table.png" }, - name = "Rustic Table", - price = 50, - itemtype = 23698, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Rustic_Trunk.png" }, - name = "Rustic Trunk", - price = 80, - itemtype = 23702, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculptor_Chair.png" }, - name = "Sculptor Chair", - price = 50, - itemtype = 34050, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculptor_Chest.png" }, - name = "Sculptor Chest", - price = 50, - itemtype = 34054, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculptor_Shelf.png" }, - name = "Sculptor Shelf", - price = 110, - itemtype = 34044, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sculptor_Table.png" }, - name = "Sculptor Table", - price = 80, - itemtype = 34048, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seafarer_Cabinet.png" }, - name = "Seafarer Cabinet", - price = 100, - itemtype = 42273, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seafarer_Chair.png" }, - name = "Seafarer Chair", - price = 60, - itemtype = 42267, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seafarer_Chest.png" }, - name = "Seafarer Chest", - price = 60, - itemtype = 42275, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Seafarer_Table.png" }, - name = "Seafarer Table", - price = 70, - itemtype = 42271, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Shroom_Cupboard.png" }, - name = "Shroom Cupboard", - price = 80, - itemtype = 37009, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Cabinet.png" }, - name = "Skeletal Cabinet", - price = 100, - itemtype = 28687, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Chair.png" }, - name = "Skeletal Chair", - price = 50, - itemtype = 28676, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Chest.png" }, - name = "Skeletal Chest", - price = 80, - itemtype = 28682, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Skeletal_Table.png" }, - name = "Skeletal Table", - price = 50, - itemtype = 28680, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Square_Side_Table.png" }, - name = "Square Side Table", - price = 50, - itemtype = 26161, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Stump_Table.png" }, - name = "Stump Table", - price = 50, - itemtype = 37008, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Toolbox.png" }, - name = "Toolbox", - price = 50, - itemtype = 35171, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Cabinet.png" }, - name = "Vengothic Cabinet", - price = 100, - itemtype = 25227, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Chair.png" }, - name = "Vengothic Chair", - price = 50, - itemtype = 25223, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Chest.png" }, - name = "Vengothic Chest", - price = 80, - itemtype = 25229, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vengothic_Table.png" }, - name = "Vengothic Table", - price = 50, - itemtype = 25225, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Cabinet.png" }, - name = "Verdant Cabinet", - price = 100, - itemtype = 26105, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Chair.png" }, - name = "Verdant Chair", - price = 50, - itemtype = 26103, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Table.png" }, - name = "Verdant Table", - price = 80, - itemtype = 26111, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Verdant_Trunk.png" }, - name = "Verdant Trunk", - price = 50, - itemtype = 26107, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Chair.png" }, - name = "Volcanic Chair", - price = 60, - itemtype = 36634, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Chest.png" }, - name = "Volcanic Chest", - price = 80, - itemtype = 36630, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Shelf.png" }, - name = "Volcanic Shelf", - price = 100, - itemtype = 36640, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Volcanic_Table.png" }, - name = "Volcanic Table", - price = 50, - itemtype = 36638, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wooden_Bookcase.png" }, - name = "Wooden Bookcase", - price = 50, - itemtype = 31194, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wooden_Cabinet.png" }, - name = "Wooden Cabinet", - price = 90, - itemtype = 35175, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Wooden_Stool.png" }, - name = "Wooden Stool", - price = 50, - itemtype = 35167, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Workbench.png" }, - name = "Workbench", - price = 90, - itemtype = 35163, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Cabinet.png" }, - name = "Zaoan Cabinet", - price = 100, - itemtype = 37782, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Hassock.png" }, - name = "Zaoan Hassock", - price = 60, - itemtype = 37778, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Zaoan_Side_Table.png" }, - name = "Zaoan Side Table", - price = 60, - itemtype = 37803, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- Beds - { - icons = { "Category_Beds.png" }, - name = "Beds", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Flower_Bed.png" }, - name = "Flower Bed", - price = 150, - itemtype = { 39788, 39789 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Grandiose_Bed.png" }, - name = "Grandiose Bed", - price = 150, - itemtype = { 35936, 35937 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Homely_Bed.png" }, - name = "Homely Bed", - price = 120, - itemtype = { 34320, 34321 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Knightly_Bed.png" }, - name = "Knightly Bed", - price = 180, - itemtype = { 39437, 39438 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Kraken_Bed.png" }, - name = "Kraken Bed", - price = 150, - itemtype = { 37201, 37202 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Log_Bed.png" }, - name = "Log Bed", - price = 150, - itemtype = { 37031, 37032 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Magnificent_Bed.png" }, - name = "Magnificent Bed", - price = 180, - itemtype = { 35859, 35860 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Opulent_Kline.png" }, - name = "Opulent Kline", - price = 120, - itemtype = { 42359, 42360 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Ornate_Bed.png" }, - name = "Ornate Bed", - price = 180, - itemtype = { 35871, 35872 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Seafarer_Bed.png" }, - name = "Seafarer Bed", - price = 180, - itemtype = { 42287, 42288 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Sleeping_Mat.png" }, - name = "Sleeping Mat", - price = 120, - itemtype = { 37793, 37794 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Vengothic_Bed.png" }, - name = "Vengothic Bed", - price = 180, - itemtype = { 35883, 35884 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Verdant_Bed.png" }, - name = "Verdant Bed", - price = 150, - itemtype = { 26096, 26097 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - { - icons = { "Wrought-Iron_Bed.png" }, - name = "Wrought-Iron Bed", - price = 150, - itemtype = { 35206, 35207 }, - count = 1, - description = "Sleep in a bed to restore soul, mana and hit points and to train your skills!\n\n{house}\n{boxicon}comes in 2 boxes which can only be unwrapped by purchasing character, put the 2 parts together to get a functional bed\n{storeinbox}\n{usablebyallicon}if not already occupied, it can be used by every Premium character that has access to the house\n{useicon}use it to sleep in it\n{backtoinbox}\n", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_BED, - }, - }, - }, - -- House ~ Upgrades - { - icons = { "Category_HouseUpgrades.png" }, - name = "Upgrades", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Reward_Shrine.png" }, - name = "Daily Reward Shrine", - price = 150, - itemtype = 25721, - count = 1, - description = "Pick up your daily reward comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it to open the reward wall\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Exercise_Dummy.png" }, - name = "Demon Exercise Dummy", - price = 900, - itemtype = 28561, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n{useicon} use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ferumbras_Exercise_Dummy.png" }, - name = "Ferumbras Exercise Dummy", - price = 900, - itemtype = 28559, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n{useicon} use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Gilded_Imbuing_Shrine.png" }, - name = "Gilded Imbuing Shrine", - price = 200, - itemtype = 25183, - count = 1, - description = "Enhance your equipment comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it with an imbuable item to open the imbuing dialog\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Imbuing_Shrine.png" }, - name = "Imbuing Shrine", - price = 150, - itemtype = 25175, - count = 1, - description = "Enhance your equipment comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it with an imbuable item to open the imbuing dialog\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Mailbox.png" }, - name = "Mailbox", - price = 150, - itemtype = 23399, - count = 1, - description = "Send your letters and parcels right from your own home!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Monk_Exercise_Dummy.png" }, - name = "Monk Exercise Dummy", - price = 900, - itemtype = 28563, - count = 1, - description = "Train your skills more effectively at home than in public on this expert exercise dummy!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{info} can only be used by one character at a time\n{useicon} use one of the exercise weapons on this dummy\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Ornate_Mailbox.png" }, - name = "Ornate Mailbox", - price = 200, - itemtype = 23401, - count = 1, - description = "Send your letters and parcels right from your own home!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Shiny_Reward_Shrine.png" }, - name = "Shiny Daily Reward Shrine", - price = 200, - itemtype = 25723, - count = 1, - description = "Pick up your daily reward comfortably in your own four walls!\n\n{house}\n{box}\n{storeinbox}\n{usablebyall}\n{useicon} use it to open the reward wall\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, - -- House ~ Hirelings - { - icons = { "Category_HouseTools_NPCApprenticeships.png" }, - name = "Hirelings", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Hireling_Male.png" }, - name = "Hireling Apprentice", - price = 150, - id = 25440, - count = 1, - number = 1, - sexId = { female = 1107, male = 1108 }, - description = "Get your very own hireling to serve you and your guests in your own four walls!\n\n{house} can only be unwrapped in a house owned by the purchasing character\n{boxicon} comes in a magic lamp which can only be used by purchasing character\n{storeinbox}\n{usablebyallicon} can be used by all characters that have access to the house\n{useicon} use the magic lamp to summon your hireling\n{backtoinbox}\n{info} maximum amount that can be owned by character: 10", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING, - }, - { - icons = { "Hireling_Male.png" }, - name = "Hireling Name Change", - price = 250, - id = 25438, - count = 1, - number = 1, - description = "{info} Change the name of one of your hirelings", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE, - }, - { - icons = { "Hireling_Male.png" }, - name = "Hireling Sex Change", - price = 120, - id = 25437, - count = 1, - number = 1, - description = "{info} Change the sex of one of your hirelings", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE, - }, - { - icons = { "Hireling_Cook.png" }, - name = "Hireling Cook", - price = 900, - id = HIRELING_SKILLS.COOKING[1], - count = 1, - number = 1, - description = "{info} Give your hirelings the ability to cook exclusive status enhancement and instant recovery meals!", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL, - }, - { - icons = { "Hireling_Trader.png" }, - name = "Hireling Trader", - price = 250, - id = HIRELING_SKILLS.TRADER[1], - count = 1, - number = 1, - description = "{info} Give your hirelings the ability of trading several types of items, including equipment, tools, potions, runes, wands and rods.", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL, - }, - { - icons = { "Hireling_Steward.png" }, - name = "Hireling Steward", - price = 250, - id = HIRELING_SKILLS.STEWARD[1], - count = 1, - number = 1, - description = "{info} Give your hirelings the ability to access and manage your stash at the confort of your from home", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL, - }, - { - icons = { "Hireling_Banker.png" }, - name = "Hireling Banker", - price = 250, - id = HIRELING_SKILLS.BANKER[1], - count = 1, - number = 1, - description = "{info} Give your hirelings the ability of managing your banking business.", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL, - }, - }, - }, - -- House ~ Hireling Dresses - { - icons = { "Category_HouseTools_NPCDresses.png" }, - name = "Hireling Dresses", - parent = "Houses", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Hireling_Banker.png" }, - name = "Banker Dress", - price = 500, - id = HIRELING_OUTFITS.BANKER[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} colours can be changed using the Outfit dialog\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Trader.png" }, - name = "Trader Dress", - price = 500, - id = HIRELING_OUTFITS.TRADER[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} colours can be changed using the Outfit dialog\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Cook.png" }, - name = "Cook Dress", - price = 500, - id = HIRELING_OUTFITS.COOKING[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} colours can be changed using the Outfit dialog\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Steward.png" }, - name = "Steward Dress", - price = 500, - id = HIRELING_OUTFITS.STEWARD[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} colours can be changed using the Outfit dialog\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Servant.png" }, - name = "Servant Dress", - price = 300, - id = HIRELING_OUTFITS.SERVANT[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} colours can be changed using the Outfit dialog\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Hydra.png" }, - name = "Hydra Dress", - price = 900, - id = HIRELING_OUTFITS.HYDRA[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Ferumbras.png" }, - name = "Ferumbras Dress", - price = 900, - id = HIRELING_OUTFITS.FERUMBRAS[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Bonelord.png" }, - name = "Bonelord Dress", - price = 900, - id = HIRELING_OUTFITS.BONELORD[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - { - icons = { "Hireling_Dragon.png" }, - name = "Dragon Dress", - price = 900, - id = HIRELING_OUTFITS.DRAGON[1], - count = 1, - number = 1, - description = "{info} can only be used for hirelings of the purchasing character\n{activated}\n{info} the purchased dress can be used by all hirelings, however, how many hirelings can wear this outfit at the same time depends on the number of dresses you own\n{info} number that can be purchased depends on the amount of hirelings you own", - type = GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT, - }, - }, - }, - -- Boost - { - icons = { "Category_Boosts.png" }, - name = "Boosts", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "XP_Boost.png" }, - name = "XP Boost", - price = 30, - id = 65010, - description = "Purchase a boost that increases the experience points your character gains from hunting by 50%!\n\n{character}\n{info} lasts for 1 hour hunting time\n{info} paused if stamina falls under 14 hours\n{info} can be purchased up to 5 times between 2 server saves\n{info} price increases with every purchase\n{info} cannot be purchased if an XP boost is already active", - type = GameStore.OfferTypes.OFFER_TYPE_EXPBOOST, - }, - }, - }, - -- Extras - { - icons = { "Category_Extras.png" }, - name = "Extras", - rookgaard = true, - subclasses = { "Extra Services", "Useful Things" }, - }, - -- Extras ~ Extras Services - { - icons = { "Category_ExtraServices.png" }, - name = "Extra Services", - parent = "Extras", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Name_Change.png" }, - name = "Character Name Change", - home = true, - price = 250, - id = 65002, - description = "Tired of your current character name? Purchase a new one!\n\n{character}\n{info} relog required after purchase to finalise the name change", - type = GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE, - }, - { - icons = { "Sex_Change.png" }, - name = "Character Sex Change", - price = 120, - id = 65003, - description = "Turns your female character into a male one - or vice versa.\n\n{character}\n{activated}\n{info} you will keep all outfits you have purchased or earned in quest", - type = GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE, - }, - }, - }, - -- Extras ~ Usefull Things - { - icons = { "Category_UsefulThings.png" }, - name = "Useful Things", - parent = "Extras", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Prey_Bonus_Reroll.png" }, - name = "Prey Wildcard", - price = 50, - id = GameStore.SubActions.PREY_WILDCARD, - count = 5, - description = "Use Prey Wildcards to reroll the bonus of an active prey, to lock your active prey or to select a prey of your choice.\n\n{character}\n{info} added directly to Prey dialog\n{info} maximum amount that can be owned by character: 50", - type = GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, - }, - { - icons = { "Instant_Reward_Access.png" }, - name = "Instant Reward Access", - price = 100, - id = GameStore.SubActions.INSTANT_REWARD, - count = 1, - description = "No matter where you are in Tibia, claim your daily reward on the spot!\n\n{character}\n{info} added to your reward wall\n{info} maximum amount that can be owned by character: 90", - type = GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS, - }, - { - icons = { "Charm_Expansion_Offer.png" }, - name = "Charm Expansion", - price = 450, - id = GameStore.SubActions.CHARM_EXPANSION, - description = "Assign as many of your unlocked Charms as you like and get a 25% discount whenever you are removing a Charm from a creature!\n\n{character}\n{once}", - type = GameStore.OfferTypes.OFFER_TYPE_CHARMS, - }, - { - icons = { "Permanent_Prey_Slot.png" }, - name = "Permanent Prey Slot", - price = 900, - id = GameStore.SubActions.PREY_THIRDSLOT_REDIRECT, - description = "Get an additional prey slot to activate additional prey!\n\n{character}\n{info} maximum amount that can be owned by character: 3\n{info} added directly to Prey dialog", - type = GameStore.OfferTypes.OFFER_TYPE_PREYSLOT, - }, - { - icons = { "Permanent_Hunting_Task_Slot.png" }, - name = "Permanent Hunting Task Slot", - price = 900, - id = GameStore.SubActions.TASKHUNTING_THIRDSLOT, - description = "Get an additional hunting tasks slot to activate additional hunting task!\n\n{character}\n{info} maximum amount that can be owned by character: 3\n{info} added directly to Hunting Task dialog", - type = GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT, - }, - { - icons = { "Gold_Converter.png" }, - name = "Gold Converter", - price = 5, - itemtype = 23722, - charges = 500, - description = "Changes either a stack of 100 gold pieces into 1 platinum coin, or a stack of 100 platinum coins into 1 crystal coin!\n\n{character}\n{storeinbox}\n{useicon} use it on a stack of 100 to change it to the superior currency\n{info} usable 500 times a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Gold_Pouch.png" }, - name = "Gold Pouch", - price = 900, - itemtype = 23721, - count = 1, - description = "Carries as many gold, platinum or crystal coins as your capacity allows, however, no other items.\n\n{character}\n{storeinbox}\n{once}\n{useicon} use it to open it\n{info} always placed on the first position of your Store inbox", - type = GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE, - }, - { - icons = { "Magic_Gold_Converter.png" }, - name = "Magic Gold Converter", - price = 15, - itemtype = 28525, - charges = 500, - description = "Changes automatically either a stack of 100 gold pieces into 1 platinum coin, or a stack of 100 platinum coins into 1 crystal coin!\n\n{character}\n{storeinbox}\n{useicon} use it to activate or deactivate the automatic conversion\n{info} converts all stacks of 100 gold or platinum in the inventory whenever it is activated\n{info} deactivated upon purchase\n{info} usable for 500 conversions a piece", - type = GameStore.OfferTypes.OFFER_TYPE_CHARGES, - }, - { - icons = { "Prey_Bonus_Reroll.png" }, - name = "Prey Wildcard", - price = 50, - count = 20, - description = "Use Prey Wildcards to reroll the bonus of an active prey, to lock your active prey or to select a prey of your choice.\n\n{character}\n{info} added directly to Prey dialog\n{info} maximum amount that can be owned by character: 50", - type = GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, - }, - { - icons = { "Temple_Teleport.png" }, - name = "Temple Teleport", - price = 15, - description = "Teleports you instantly to your home temple.\n\n{character}\n{useicon} use it to teleport you to your home temple\n{battlesign}\n{info} does not work in no-logout zones or close to a character's home temple", - type = GameStore.OfferTypes.OFFER_TYPE_TEMPLE, - }, - }, - }, - -- Tournament - { - icons = { "Category_Tournament.png" }, - name = "Tournament", - rookgaard = true, - subclasses = { "Tickets", "Exclusive Offers" }, - }, - -- Tournament ~ Tickets - { - icons = { "Category_Tickets.png" }, - parent = "Tournament", - name = "Tickets", - rookgaard = true, - offers = { - { - icons = { "Tournament_Restricted.png" }, - name = "Restricted Tournament Ticket", - price = 500, - }, - }, - }, - -- Tournament ~ Exclusive Offers - { - icons = { "Category_ExclusiveOffers.png" }, - name = "Exclusive Offers", - parent = "Tournament", - rookgaard = true, - state = GameStore.States.STATE_NONE, - offers = { - { - icons = { "Baby_Brain_Squid.png" }, - name = "Baby Brain Squid", - price = 800, - itemtype = 32909, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Baby_Vulcongra.png" }, - name = "Baby Vulcongra", - price = 800, - itemtype = 32908, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table.png" }, - name = "Carved Table", - price = 100, - itemtype = 32972, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table_Centre.png" }, - name = "Carved Table Centre", - price = 100, - itemtype = 32974, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Carved_Table_Corner.png" }, - name = "Carved Table Corner", - price = 100, - itemtype = 32969, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cerberus_Champion.png" }, - name = "Cerberus Champion", - price = 1250, - id = 146, - description = "{info} usable by all characters of the account\n{speedboost}\n\nA fierce and grim guardian of the underworld has risen to fight side by side with the bravest warriors in order to send evil creatures into the realm of the dead. The three headed Cerberus Champion is constantly baying for blood and using its sharp fangs it easily rips apart even the strongest armour and shield.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Cerberus_Champion_Puppy.png" }, - name = "Cerberus Champion Puppy", - price = 800, - itemtype = 31464, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch.png" }, - name = "Cozy Couch", - price = 100, - itemtype = 32948, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Left_End.png" }, - name = "Cozy Couch Left End", - price = 100, - itemtype = 32952, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Right_End.png" }, - name = "Cozy Couch Right End", - price = 100, - itemtype = 32956, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Cozy_Couch_Corner.png" }, - name = "Cozy Couch Corner", - price = 100, - itemtype = 32964, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Demon_Doll.png" }, - name = "Demon Doll", - price = 400, - itemtype = 32918, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Outfit_Dragon_Slayer_Male_Addon_3.png", "Outfit_Dragon_Slayer_Female_Addon_3.png" }, - name = "Full Dragon Slayer Outfit", - price = 5000, - sexId = { female = 1289, male = 1288 }, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe souls of countless slain dragons have been fused over the years with this armour, wrought from the impervious scales of the ancestors of those very same beings, wicked and wise, winged and wild. The Dragon Slayer Outfit has seen an unfathomable amount of bloodshed, but it pales in comparison to the untold lives lost in the strife over the armour itself. Only the mightiest warriors can even begin to dream of ever owning this exceedingly rare token of power.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Gilded_Blessed_Shield.png" }, - name = "Gilded Blessed Shield", - price = 1500, - itemtype = 37165, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Gilded_Crown.png" }, - name = "Gilded Crown", - price = 1500, - itemtype = 34332, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Gilded_Horned_Helmet.png" }, - name = "Gilded Horned Helmet", - price = 1500, - itemtype = 38640, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Gilded_Magic_Longsword.png" }, - name = "Gilded Magic Longsword", - price = 1500, - itemtype = 36995, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Gilded_Warlord_Sword.png" }, - name = "Gilded Warlord Sword", - price = 1500, - itemtype = 39767, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - home = true, - }, - { - icons = { "Guzzlemaw_Grub.png" }, - name = "Guzzlemaw Grub", - price = 800, - itemtype = 32907, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Jousting_Eagle.png" }, - name = "Jousting Eagle", - price = 1250, - id = 145, - description = "{info} usable by all characters of the account\n{speedboost}\n\nHigh above the clouds far away from dry land, the training of giant eagles takes place. Only the cream of the crop is able to survive in such harsh environment long enough to call themselves Jousting Eagles while the weaklings find themselves at the bottom of the sea. The tough ones become noble and graceful mounts that are well known for their agility and endurance.", - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - }, - { - icons = { "Jousting_Eagle_Baby.png" }, - name = "Jousting Eagle Baby", - price = 800, - itemtype = 31462, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{use}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Outfit_Lion_of_War_Male_Addon_3.png", "Outfit_Lion_of_War_Female_Addon_3.png" }, - name = "Full Lion of War Outfit", - price = 1750, - sexId = { female = 1207, male = 1206 }, - addon = 3, - description = "{info} usable by all characters of the account\n{info} colours can be changed using the Outfit dialog\n{info} includes basic outfit and 2 addons which can be selected individually\n\nThe Lion of War has fought on countless battlefields and never lost once. Enemies tremble with fear when he batters his sword against his almighty shield. Realising that a Lion of War knows no mercy, his opponents often surrender before the battle even begins.", - type = GameStore.OfferTypes.OFFER_TYPE_OUTFIT, - }, - { - icons = { "Ogre_Rowdy_Doll.png" }, - name = "Ogre Rowdy Doll", - price = 400, - itemtype = 32944, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Retching_Horror_Doll.png" }, - name = "Retching Horror Doll", - price = 400, - itemtype = 32945, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sublime_Tournament_Accolade.png" }, - name = "Sublime Tournament Accolade", - price = 500, - itemtype = 31472, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Sublime_Tournament_Carpet.png" }, - name = "Sublime Tournament Carpet", - price = 70, - itemtype = 31467, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use an unwrapped carpet to roll it out or up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tournament_Accolade.png" }, - name = "Tournament Accolade", - price = 500, - itemtype = 31470, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Tournament_Carpet.png" }, - name = "Tournament Carpet", - price = 70, - itemtype = 31466, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{useicon} use an unwrapped carpet to roll it out or up\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - { - icons = { "Vexclaw_Doll.png" }, - name = "Vexclaw Doll", - price = 400, - itemtype = 32943, - count = 1, - description = "{house}\n{box}\n{storeinbox}\n{backtoinbox}", - type = GameStore.OfferTypes.OFFER_TYPE_HOUSE, - }, - }, - }, -} - --- Each outfit must be uniquely identified to distinguish between addons. --- Here we dynamically assign ids for outfits. These ids must be unique. -local runningId = 45000 -for k, category in ipairs(GameStore.Categories) do - if category.offers then - for m, offer in ipairs(category.offers) do - if not offer.id then - if type(offer.count) == "table" then - for i = 1, #offer.price do - offer.id[i] = runningId - runningId = runningId + 1 - end - else - offer.id = runningId - runningId = runningId + 1 - end - end - if not offer.type then - offer.type = GameStore.OfferTypes.OFFER_TYPE_NONE - end - if not offer.coinType then - offer.coinType = GameStore.CoinType.Transferable - end - end - end -end diff --git a/data/modules/scripts/gamestore/init.lua b/data/modules/scripts/gamestore/init.lua deleted file mode 100644 index 433abf34492..00000000000 --- a/data/modules/scripts/gamestore/init.lua +++ /dev/null @@ -1,2268 +0,0 @@ -GameStore = { - ModuleName = "GameStore", - Developers = { "Cjaker", "metabob", "Rick" }, - Version = "1.1", - LastUpdated = "25-07-2020 11:52AM", -} - ---== Enums ==-- -GameStore.OfferTypes = { - OFFER_TYPE_NONE = 0, - OFFER_TYPE_ITEM = 1, - OFFER_TYPE_STACKABLE = 2, - OFFER_TYPE_CHARGES = 3, - OFFER_TYPE_OUTFIT = 4, - OFFER_TYPE_OUTFIT_ADDON = 5, - OFFER_TYPE_MOUNT = 6, - OFFER_TYPE_NAMECHANGE = 7, - OFFER_TYPE_SEXCHANGE = 8, - OFFER_TYPE_HOUSE = 9, - OFFER_TYPE_EXPBOOST = 10, - OFFER_TYPE_PREYSLOT = 11, - OFFER_TYPE_PREYBONUS = 12, - OFFER_TYPE_TEMPLE = 13, - OFFER_TYPE_BLESSINGS = 14, - OFFER_TYPE_PREMIUM = 15, - -- 16, -- Empty - OFFER_TYPE_ALLBLESSINGS = 17, - OFFER_TYPE_INSTANT_REWARD_ACCESS = 18, - OFFER_TYPE_CHARMS = 19, - OFFER_TYPE_HIRELING = 20, - OFFER_TYPE_HIRELING_NAMECHANGE = 21, - OFFER_TYPE_HIRELING_SEXCHANGE = 22, - OFFER_TYPE_HIRELING_SKILL = 23, - OFFER_TYPE_HIRELING_OUTFIT = 24, - OFFER_TYPE_HUNTINGSLOT = 25, - OFFER_TYPE_ITEM_BED = 26, - OFFER_TYPE_ITEM_UNIQUE = 27, -} - -GameStore.SubActions = { - PREY_THIRDSLOT_REAL = 0, - PREY_WILDCARD = 1, - INSTANT_REWARD = 2, - BLESSING_TWIST = 3, - BLESSING_SOLITUDE = 4, - BLESSING_PHOENIX = 5, - BLESSING_SUNS = 6, - BLESSING_SPIRITUAL = 7, - BLESSING_EMBRACE = 8, - BLESSING_BLOOD = 9, - BLESSING_HEART = 10, - BLESSING_ALL_PVE = 11, - BLESSING_ALL_PVP = 12, - CHARM_EXPANSION = 13, - TASKHUNTING_THIRDSLOT = 14, - PREY_THIRDSLOT_REDIRECT = 15, -} - -GameStore.ActionType = { - OPEN_HOME = 0, - OPEN_PREMIUM_BOOST = 1, - OPEN_CATEGORY = 2, - OPEN_USEFUL_THINGS = 3, - OPEN_OFFER = 4, - OPEN_SEARCH = 5, -} - -GameStore.CoinType = { - Coin = 0, - Transferable = 1, -} - -GameStore.Storages = { - expBoostCount = 51052, -} - -GameStore.ConverType = { - SHOW_NONE = 0, - SHOW_MOUNT = 1, - SHOW_OUTFIT = 2, - SHOW_ITEM = 3, - SHOW_HIRELING = 4, -} - -GameStore.ConfigureOffers = { - SHOW_NORMAL = 0, - SHOW_CONFIGURE = 1, -} - -function convertType(type) - local types = { - [GameStore.OfferTypes.OFFER_TYPE_OUTFIT] = GameStore.ConverType.SHOW_OUTFIT, - [GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON] = GameStore.ConverType.SHOW_OUTFIT, - [GameStore.OfferTypes.OFFER_TYPE_MOUNT] = GameStore.ConverType.SHOW_MOUNT, - [GameStore.OfferTypes.OFFER_TYPE_ITEM] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_STACKABLE] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_HOUSE] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_CHARGES] = GameStore.ConverType.SHOW_ITEM, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING] = GameStore.ConverType.SHOW_HIRELING, - [GameStore.OfferTypes.OFFER_TYPE_ITEM_BED] = GameStore.ConverType.SHOW_NONE, - [GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE] = GameStore.ConverType.SHOW_ITEM, - } - - if not types[type] then - return GameStore.ConverType.SHOW_NONE - end - - return types[type] -end - -function useOfferConfigure(type) - local types = { - [GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - [GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE] = GameStore.ConfigureOffers.SHOW_CONFIGURE, - } - - if not types[type] then - return GameStore.ConfigureOffers.SHOW_NORMAL - end - - return types[type] -end - -GameStore.ClientOfferTypes = { - CLIENT_STORE_OFFER_OTHER = 0, - CLIENT_STORE_OFFER_NAMECHANGE = 1, - CLIENT_STORE_OFFER_HIRELING = 3, -} - -GameStore.HistoryTypes = { - HISTORY_TYPE_NONE = 0, - HISTORY_TYPE_GIFT = 1, - HISTORY_TYPE_REFUND = 2, -} - -GameStore.States = { - STATE_NONE = 0, - STATE_NEW = 1, - STATE_SALE = 2, - STATE_TIMED = 3, -} - -GameStore.StoreErrors = { - STORE_ERROR_PURCHASE = 0, - STORE_ERROR_NETWORK = 1, - STORE_ERROR_HISTORY = 2, - STORE_ERROR_TRANSFER = 3, - STORE_ERROR_INFORMATION = 4, -} - -GameStore.ServiceTypes = { - SERVICE_STANDERD = 0, - SERVICE_OUTFITS = 3, - SERVICE_MOUNTS = 4, - SERVICE_BLESSINGS = 5, -} - -GameStore.SendingPackets = { - S_CoinBalance = 0xDF, -- 223 - S_StoreError = 0xE0, -- 224 - S_RequestPurchaseData = 0xE1, -- 225 - S_CoinBalanceUpdating = 0xF2, -- 242 - S_OpenStore = 0xFB, -- 251 - S_StoreOffers = 0xFC, -- 252 - S_OpenTransactionHistory = 0xFD, -- 253 - S_CompletePurchase = 0xFE, -- 254 -} - -GameStore.RecivedPackets = { - C_StoreEvent = 0xE9, -- 233 - C_TransferCoins = 0xEF, -- 239 - C_ParseHirelingName = 0xEC, -- 236 - C_OpenStore = 0xFA, -- 250 - C_RequestStoreOffers = 0xFB, -- 251 - C_BuyStoreOffer = 0xFC, -- 252 - C_OpenTransactionHistory = 0xFD, -- 253 - C_RequestTransactionHistory = 0xFE, -- 254 -} - -GameStore.ExpBoostValues = { - [1] = 30, - [2] = 45, - [3] = 90, - [4] = 180, - [5] = 360, -} - -GameStore.DefaultValues = { - DEFAULT_VALUE_ENTRIES_PER_PAGE = 26, -} - -GameStore.DefaultDescriptions = { - OUTFIT = { "This outfit looks nice. Only high-class people are able to wear it!", "An outfit that was created to suit you. We are sure you'll like it.", "Legend says only smart people should wear it, otherwise you will burn!" }, - MOUNT = { "This is a fantastic mount that helps to become faster, try it!", "The first rider of this mount became the leader of his country! legends say that." }, - NAMECHANGE = { "Are you hunted? Tired of that? Get a new name, a new life!", "A new name to suit your needs!" }, - SEXCHANGE = { "Bored of your character's sex? Get a new sex for him now!!" }, - EXPBOOST = { "Are you tired of leveling slow? try it!" }, - PREYSLOT = { - "It's hunting season! Activate a prey to gain a bonus when hunting a certain monster. Every character can purchase one Permanent Prey Slot, which enables the activation of an additional prey. \nIf you activate a prey, you can select one monster out of nine. The bonus for your prey will be selected randomly from one of the following: damage boost, damage reduction, bonus XP, improved loot. The bonus value may range from 5% to 50%. Your prey will be active for 2 hours hunting time: the duration of an active prey will only be reduced while you are hunting.", - }, - PREYBONUS = { - "You activated a prey but do not like the randomly selected bonus? Roll for a new one! Here you can purchase five Prey Bonus Rerolls! \nA Bonus Reroll allows you to get a bonus with a higher value (max. 50%). The bonus for your prey will be selected randomly from one of the following: damage boost, damage reduction, bonus XP, improved loot. The 2 hours hunting time will start anew once you have rolled for a new bonus. Your prey monster will stay the same.", - }, - TEMPLE = { "Need a quick way home? Buy this transportation service to get instantly teleported to your home temple. \n\nNote, you cannot use this service while having a battle sign or a protection zone block. Further, the service will not work in no-logout zones or close to your home temple." }, -} - -GameStore.ItemLimit = { - PREY_WILDCARD = 50, - INSTANT_REWARD_ACCESS = 90, - EXPBOOST = 6, - HIRELING = 10, -} - ---==Parsing==-- -GameStore.isItsPacket = function(byte) - for k, v in pairs(GameStore.RecivedPackets) do - if v == byte then - return true - end - end - return false -end - -function GameStore.fuzzySearchOffer(searchString) - local results = {} - for i, category in ipairs(GameStore.Categories) do - if category.offers then - for j, offer in ipairs(category.offers) do - if string.match(offer.name:lower(), searchString:lower()) then - table.insert(results, offer) - end - end - end - end - return results -end - -local function queueSendStoreAlertToUser(message, delay, playerId, storeErrorCode) - storeErrorCode = storeErrorCode and storeErrorCode or GameStore.StoreErrors.STORE_ERROR_NETWORK - addPlayerEvent(sendStoreError, delay, playerId, storeErrorCode, message) -end - -function onRecvbyte(player, msg, byte) - if player:getVocation():getId() == 0 and not GameStore.haveCategoryRook() then - return player:sendCancelMessage("Store don't have offers for rookgaard citizen.") - end - - if player:isUIExhausted(250) then - player:sendCancelMessage("You are exhausted.") - return - end - - if byte == GameStore.RecivedPackets.C_StoreEvent then - elseif byte == GameStore.RecivedPackets.C_TransferCoins then - parseTransferableCoins(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_OpenStore then - parseOpenStore(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_RequestStoreOffers then - parseRequestStoreOffers(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_BuyStoreOffer then - parseBuyStoreOffer(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_OpenTransactionHistory then - parseOpenTransactionHistory(player:getId(), msg) - elseif byte == GameStore.RecivedPackets.C_RequestTransactionHistory then - parseRequestTransactionHistory(player:getId(), msg) - end - - return true -end - -function parseTransferableCoins(playerId, msg) - local player = Player(playerId) - if not player then - return false - end - - local reciver = msg:getString() - local amount = msg:getU32() - - if player:getTransferableCoins() < amount then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You don't have this amount of coins.") - end - - if reciver:lower() == player:getName():lower() then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You can't transfer coins to yourself.") - end - - local resultId = db.storeQuery("SELECT `account_id` FROM `players` WHERE `name` = " .. db.escapeString(reciver:lower()) .. "") - if not resultId then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "We couldn't find that player.") - end - - local accountId = Result.getNumber(resultId, "account_id") - if accountId == player:getAccountId() then - return addPlayerEvent(sendStoreError, 350, playerId, GameStore.StoreErrors.STORE_ERROR_TRANSFER, "You cannot transfer coin to a character in the same account.") - end - - db.query("UPDATE `accounts` SET `coins_transferable` = `coins_transferable` + " .. amount .. " WHERE `id` = " .. accountId) - player:removeTransferableCoinsBalance(amount) - addPlayerEvent(sendStorePurchaseSuccessful, 550, playerId, "You have transfered " .. amount .. " coins to " .. reciver .. " successfully") - - -- Adding history for both receiver/sender - GameStore.insertHistory(accountId, GameStore.HistoryTypes.HISTORY_TYPE_NONE, player:getName() .. " transferred you this amount.", amount, GameStore.CoinType.Transferable) - GameStore.insertHistory(player:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, "You transferred this amount to " .. reciver, -1 * amount, GameStore.CoinType.Transferable) - openStore(playerId) - player:updateUIExhausted() -end - -function parseOpenStore(playerId, msg) - openStore(playerId) - - local category = GameStore.Categories and GameStore.Categories[1] or nil - if category then - addPlayerEvent(parseRequestStoreOffers, 50, playerId) - end -end - -function parseRequestStoreOffers(playerId, msg) - local player = Player(playerId) - if not player then - return false - end - - local actionType = msg:getByte() - local oldProtocol = player:getClient().version < 1200 - - if oldProtocol then - local stringParam = msg:getString() - local category = GameStore.getCategoryByName(stringParam) - if category then - addPlayerEvent(sendShowStoreOffersOnOldProtocol, 350, playerId, category) - end - elseif actionType == GameStore.ActionType.OPEN_CATEGORY then - local stringParam = msg:getString() - local category = GameStore.getCategoryByName(stringParam) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end - elseif actionType == GameStore.ActionType.OPEN_HOME then - sendHomePage(player:getId()) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, "Home Offers") - end - elseif actionType == GameStore.ActionType.OPEN_PREMIUM_BOOST then - local subAction = msg:getByte() - local category = nil - - local premiumCategoryName = "Premium Time" - if configManager.getBoolean(configKeys.VIP_SYSTEM_ENABLED) then - premiumCategoryName = "VIP Shop" - end - if subAction == 0 then - category = GameStore.getCategoryByName(premiumCategoryName) - else - category = GameStore.getCategoryByName("Boosts") - end - - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end - elseif actionType == GameStore.ActionType.OPEN_USEFUL_THINGS then - local subAction = msg:getByte() - local offerId = subAction - local category = nil - if subAction >= GameStore.SubActions.BLESSING_TWIST and subAction <= GameStore.SubActions.BLESSING_ALL_PVP then - category = GameStore.getCategoryByName("Blessings") - else - category = GameStore.getCategoryByName("Useful Things") - end - - if subAction == GameStore.SubActions.PREY_THIRDSLOT_REAL then - offerId = GameStore.SubActions.PREY_THIRDSLOT_REDIRECT - end - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category, offerId) - end - elseif actionType == GameStore.ActionType.OPEN_OFFER then - local offerId = msg:getU32() - local category = GameStore.getCategoryByOffer(offerId) - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category, offerId) - end - elseif actionType == GameStore.ActionType.OPEN_SEARCH then - local searchString = msg:getString() - local results = GameStore.fuzzySearchOffer(searchString) - if not results or #results == 0 then - return addPlayerEvent(sendStoreError, 250, playerId, GameStore.StoreErrors.STORE_ERROR_INFORMATION, 'No results found for "' .. searchString .. '".') - end - - local searchResultsCategory = { - name = "Search", - offers = results, - } - - addPlayerEvent(sendShowStoreOffers, 250, playerId, searchResultsCategory) - end - player:updateUIExhausted() -end - --- Used on cyclopedia store summary -local function insertPlayerTransactionSummary(player, offer) - local id = offer.id - if offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE then - id = offer.itemtype - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then - id = offer.blessid - end - player:createTransactionSummary(offer.type, math.max(1, offer.count or 1), id) -end - -function parseBuyStoreOffer(playerId, msg) - local player = Player(playerId) - local id = msg:getU32() - local offer = GameStore.getOfferById(id) - local productType = msg:getByte() - if not offer then - return false - end - - -- All guarding conditions under which the offer should not be processed must be included here - if - not table.contains(GameStore.OfferTypes, offer.type) -- we've got an invalid offer type - or not player - or (player:getVocation():getId() == 0) and (not GameStore.haveOfferRook(id)) -- we don't have such offer - or not offer - or (offer.type == GameStore.OfferTypes.OFFER_TYPE_NONE) -- offer is disabled - or ( - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_EXPBOOST - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYBONUS - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYSLOT - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_TEMPLE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT - and not offer.id - ) - then - return queueSendStoreAlertToUser("This offer is unavailable [1]", 350, playerId, GameStore.StoreErrors.STORE_ERROR_INFORMATION) - end - - -- At this point the purchase is assumed to be formatted correctly - local offerPrice = offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] or offer.price - local offerCoinType = offer.coinType - if offer.type == GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE and player:kv():get("namelock") then - offerPrice = 0 - end - -- Check if offer can be honored - if offerPrice > 0 and not player:canPayForOffer(offerPrice, offerCoinType) then - return queueSendStoreAlertToUser("You don't have enough coins. Your purchase has been cancelled.", 250, playerId) - end - - -- Use pcall to catch unhandled errors and send an alert to the user because the client expects it at all times; (OTClient will unlock UI) - -- Handled errors are thrown to indicate that the purchase has failed; - -- Handled errors have a code index and unhandled errors do not - local pcallOk, pcallError = pcall(function() - if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then - GameStore.processItemPurchase(player, offer.itemtype, offer.count or 1, offer.movable, offer.setOwner) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then - GameStore.processInstantRewardAccess(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARMS then - GameStore.processCharmsPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then - GameStore.processSingleBlessingPurchase(player, offer.blessid, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS then - GameStore.processAllBlessingsPurchase(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREMIUM then - GameStore.processPremiumPurchase(player, offer.id) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_STACKABLE then - GameStore.processStackablePurchase(player, offer.itemtype, offer.count, offer.name, offer.movable, offer.setOwner) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HOUSE or offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_BED then - GameStore.processHouseRelatedPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then - GameStore.processOutfitPurchase(player, offer.sexId, offer.addon) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then - GameStore.processMountPurchase(player, offer.id) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE then - local newName = msg:getString() - GameStore.processNameChangePurchase(player, offer, productType, newName) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE then - GameStore.processSexChangePurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then - GameStore.processExpBoostPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT then - GameStore.processTaskHuntingThirdSlot(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYSLOT then - GameStore.processPreyThirdSlot(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYBONUS then - GameStore.processPreyBonusReroll(player, offer.count) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_TEMPLE then - GameStore.processTempleTeleportPurchase(player) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARGES then - GameStore.processChargesPurchase(player, offer.itemtype, offer.name, offer.charges, offer.movable, offer.setOwner) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING then - local hirelingName = msg:getString() - local sex = msg:getByte() - GameStore.processHirelingPurchase(player, offer, productType, hirelingName, sex) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE then - local hirelingName = msg:getString() - GameStore.processHirelingChangeNamePurchase(player, offer, productType, hirelingName) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE then - GameStore.processHirelingChangeSexPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL then - GameStore.processHirelingSkillPurchase(player, offer) - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then - GameStore.processHirelingOutfitPurchase(player, offer) - else - -- This should never happen by our convention, but just in case the guarding condition is messed up... - error({ code = 0, message = "This offer is unavailable [2]" }) - end - end) - - if not pcallOk then - local alertMessage = pcallError.code and pcallError.message or "Something went wrong. Your purchase has been cancelled." - - -- unhandled error - if not pcallError.code then - logger.warn("[parseBuyStoreOffer] - Purchase failed due to an unhandled script error. Stacktrace: {}", pcallError) - end - - return queueSendStoreAlertToUser(alertMessage, 500, playerId) - end - - if table.contains({ GameStore.OfferTypes.OFFER_TYPE_HOUSE, GameStore.OfferTypes.OFFER_TYPE_EXPBOOST, GameStore.OfferTypes.OFFER_TYPE_PREYBONUS, GameStore.OfferTypes.OFFER_TYPE_BLESSINGS, GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS, GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS }, offer.type) then - insertPlayerTransactionSummary(player, offer) - end - local configure = useOfferConfigure(offer.type) - if configure ~= GameStore.ConfigureOffers.SHOW_CONFIGURE then - if not player:makeCoinTransaction(offer) then - return player:showInfoModal("Error", "Purchase transaction error") - end - - local message = string.format("You have purchased %s for %d coins.", offer.name, offerPrice) - sendUpdatedStoreBalances(playerId) - return addPlayerEvent(sendStorePurchaseSuccessful, 650, playerId, message) - end - - player:updateUIExhausted() - return true -end - --- Both functions use same formula! -function parseOpenTransactionHistory(playerId, msg) - local player = Player(playerId) - if not player then - return - end - - local page = 1 - GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE = msg:getByte() - sendStoreTransactionHistory(playerId, page, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) - player:updateUIExhausted() -end - -function parseRequestTransactionHistory(playerId, msg) - local player = Player(playerId) - if not player then - return - end - - local page = msg:getU32() - sendStoreTransactionHistory(playerId, page + 1, GameStore.DefaultValues.DEFAULT_VALUE_ENTRIES_PER_PAGE) - player:updateUIExhausted() -end - -local function getCategoriesRook() - local tmpTable, count = {}, 0 - for i, v in pairs(GameStore.Categories) do - if v.rookgaard then - tmpTable[#tmpTable + 1] = v - count = count + 1 - end - end - - return tmpTable, count -end - ---==Sending==-- -function openStore(playerId) - local player = Player(playerId) - if not player then - return false - end - - if not GameStore.Categories then - return false - end - - local oldProtocol = player:getClient().version < 1200 - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_OpenStore) - if oldProtocol then - msg:addByte(0x00) - end - - local GameStoreCategories, GameStoreCount = nil, 0 - if player:getVocation():getId() == 0 then - GameStoreCategories, GameStoreCount = getCategoriesRook() - else - GameStoreCategories, GameStoreCount = GameStore.Categories, #GameStore.Categories - end - local addCategory = function(category) - msg:addString(category.name, "openStore - category.name") - if oldProtocol then - msg:addString(category.description, "openStore - category.description") - end - - msg:addByte(category.state or GameStore.States.STATE_NONE) - local size = #category.icons > 255 and 255 or #category.icons - msg:addByte(size) - for _, icon in ipairs(category.icons) do - if size > 0 then - msg:addString(icon, "openStore - icon") - size = size - 1 - end - end - - if category.parent then - msg:addString(category.parent, "openStore - category.parent") - else - msg:addU16(0) - end - end - - if GameStoreCategories then - msg:addU16(GameStoreCount) - for _, category in ipairs(GameStoreCategories) do - addCategory(category) - end - msg:sendToPlayer(player) - sendStoreBalanceUpdating(playerId, true) - end -end - -function sendOfferDescription(player, offerId, description) - local msg = NetworkMessage() - msg:addByte(0xEA) - msg:addU32(offerId) - msg:addString(description, "sendOfferDescription - description") - msg:sendToPlayer(player) -end - -function Player.canBuyOffer(self, offer) - local disabled, disabledReason = 0, "" - if offer.disabled or not offer.type then - disabled = 1 - end - - if - offer.type ~= GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_EXPBOOST - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYSLOT - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_PREYBONUS - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_TEMPLE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_SEXCHANGE - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL - and offer.type ~= GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT - and not offer.id - then - disabled = 1 - end - - if disabled == 1 and offer.disabledReason then - -- dynamic disable - disabledReason = offer.disabledReason - end - - if disabled ~= 1 then - if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM_UNIQUE then - local item = self:getItemById(offer.itemtype, true) - if item then - disabled = 1 - disabledReason = "You already have a " .. ItemType(item:getId()):getName() .. "." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_BLESSINGS then - if self:getBlessingCount(offer.blessid) >= 5 then - disabled = 1 - disabledReason = "You reached the maximum amount for this blessing." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_ALLBLESSINGS then - for i = 1, 8 do - if self:getBlessingCount(i) >= 5 then - disabled = 1 - disabledReason = "You already have all Blessings." - break - end - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then - local outfitLookType - if self:getSex() == PLAYERSEX_MALE then - outfitLookType = offer.sexId.male - else - outfitLookType = offer.sexId.female - end - - if outfitLookType then - if offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT and self:hasOutfit(outfitLookType) then - disabled = 1 - disabledReason = "You already have this outfit." - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON then - if self:hasOutfit(outfitLookType) then - if self:hasOutfit(outfitLookType, offer.addon) then - disabled = 1 - disabledReason = "You already have this addon." - end - else - disabled = 1 - disabledReason = "You don't have the outfit, you can't buy the addon." - end - end - else - disabled = 1 - disabledReason = "The offer is fake." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then - if self:hasMount(offer.id) then - disabled = 1 - disabledReason = "You already have this mount." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_INSTANT_REWARD_ACCESS then - if self:getCollectionTokens() >= GameStore.ItemLimit.INSTANT_REWARD_ACCESS then - disabled = 1 - disabledReason = "You already have maximum of reward tokens." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYBONUS then - if self:getPreyCards() >= GameStore.ItemLimit.PREY_WILDCARD then - disabled = 1 - disabledReason = "You already have maximum of prey wildcards." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_CHARMS then - if self:charmExpansion() then - disabled = 1 - disabledReason = "You already have charm expansion." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HUNTINGSLOT then - if self:taskHuntingThirdSlot() then - disabled = 1 - disabledReason = "You already have 3 slots released." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_PREYSLOT then - if self:preyThirdSlot() then - disabled = 1 - disabledReason = "You already have 3 slots released." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then - if self:getStorageValue(GameStore.Storages.expBoostCount) == GameStore.ItemLimit.EXPBOOST then - disabled = 1 - disabledReason = "You can't buy XP Boost for today." - end - if self:getXpBoostTime() > 0 then - disabled = 1 - disabledReason = "You already have an active XP boost." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING then - if self:getHirelingsCount() >= GameStore.ItemLimit.HIRELING then - disabled = 1 - disabledReason = "You already have bought the maximum number of allowed hirelings." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SKILL then - if self:hasHirelingSkill(GetHirelingSkillNameById(offer.id)) then - disabled = 1 - disabledReason = "This skill is already unlocked." - end - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_OUTFIT then - if self:hasHirelingOutfit(GetHirelingOutfitNameById(offer.id)) then - disabled = 1 - disabledReason = "This hireling outfit is already unlocked." - end - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_NAMECHANGE then - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_HIRELING_SEXCHANGE then - if self:getHirelingsCount() <= 0 then - disabled = 1 - disabledReason = "You need to have a hireling." - end - end - end - - return { disabled = disabled, disabledReason = disabledReason } -end - -function Player.canReceiveStoreItems(self, offerId, offerCount) - local inbox = self:getStoreInbox() - if not inbox then - return false, "No store inbox found." - end - - local itemType = ItemType(offerId) - local slotsNeeded = offerCount or 1 - if itemType and itemType:isStackable() then - slotsNeeded = math.ceil(slotsNeeded / itemType:getStackSize()) - end - - local inboxItems = inbox:getItems(true) - local slotsOccupied = #inboxItems - local maxCapacity = inbox:getMaxCapacity() - - if slotsOccupied + slotsNeeded > maxCapacity then - local slotsAvailable = maxCapacity - slotsOccupied - return false, string.format("Not enough free slots in your store inbox. You need %d more slot(s). Currently occupied: %d/%d", slotsNeeded - slotsAvailable, slotsOccupied, maxCapacity) - end - - local totalWeight = itemType:getWeight(offerCount or 1) - if self:getFreeCapacity() < totalWeight then - return false, "Please make sure you have enough free capacity to hold this item." - end - - return true, "" -end - -function sendShowStoreOffers(playerId, category, redirectId) - local player = Player(playerId) - if not player then - return false - end - - local oldProtocol = player:getClient().version < 1200 - - local msg = NetworkMessage() - local haveSaleOffer = 0 - msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString(category.name, "sendShowStoreOffers - category.name") - - local categoryLimit = 65535 - if oldProtocol then - categoryLimit = 30 - elseif category.offers then - categoryLimit = #category.offers > categoryLimit and categoryLimit or #category.offers - else - categoryLimit = 0 - end - - if not oldProtocol then - msg:addU32(redirectId or 0) - msg:addByte(0) -- Window Type - msg:addByte(0) -- Collections Size - msg:addU16(0) -- Collection Name - end - - if not category.offers then - msg:addU16(0) -- Disable reasons - msg:addU16(0) -- Offers - msg:sendToPlayer(player) - return - end - - local disableReasons = {} - local offers = {} - local count = 0 - for k, offer in ipairs(category.offers) do - local name = offer.name or "Something Special" - if not offers[name] then - offers[name] = {} - count = count + 1 - offers[name].offers = {} - offers[name].state = offer.state - offers[name].id = offer.id - offers[name].type = offer.type - offers[name].icons = offer.icons - offers[name].basePrice = offer.basePrice - offers[name].description = offer.description - if offer.sexId then - offers[name].sexId = offer.sexId - end - if offer.itemtype then - offers[name].itemtype = offer.itemtype - end - end - - local canBuy = player:canBuyOffer(offer) - if canBuy.disabled == 1 then - for index, disableTable in ipairs(disableReasons) do - if canBuy.disabledReason == disableTable.reason then - offer.disabledReadonIndex = index - end - end - - if offer.disabledReadonIndex == nil then - offer.disabledReadonIndex = #disableReasons - table.insert(disableReasons, canBuy.disabledReason) - end - end - - table.insert(offers[name].offers, offer) - end - - msg:addU16(#disableReasons) - for _, reason in ipairs(disableReasons) do - msg:addString(reason, "sendShowStoreOffers - reason") - end - - if count > categoryLimit then - count = categoryLimit - end - - msg:addU16(count) - for name, offer in pairs(offers) do - if count > 0 then - count = count - 1 - msg:addString(name, "sendShowStoreOffers - name") - msg:addByte(#offer.offers) - sendOfferDescription(player, offer.id and offer.id or 0xFFFF, offer.description) - for _, off in ipairs(offer.offers) do - xpBoostPrice = nil - if offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST then - xpBoostPrice = GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] - end - - nameLockPrice = nil - if offer.type == GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE and player:kv():get("namelock") then - nameLockPrice = 0 - end - - msg:addU32(off.id) - msg:addU16(off.count or off.charges) - msg:addU32(xpBoostPrice or nameLockPrice or off.price) - msg:addByte(off.coinType or 0x00) - - msg:addByte((off.disabledReadonIndex ~= nil) and 1 or 0) - if off.disabledReadonIndex ~= nil then - msg:addByte(0x01) - msg:addU16(off.disabledReadonIndex) - off.disabledReadonIndex = nil -- Reseting the table to nil disable reason - end - - if off.state then - if off.state == GameStore.States.STATE_SALE then - local daySub = off.validUntil - os.date("*t").day - if daySub >= 0 then - msg:addByte(off.state) - msg:addU32(os.time() + daySub * 86400) - msg:addU32(off.basePrice) - haveSaleOffer = 1 - else - msg:addByte(GameStore.States.STATE_NONE) - end - else - msg:addByte(off.state) - end - else - msg:addByte(GameStore.States.STATE_NONE) - end - end - - local tryOnType = 0 - local type = convertType(offer.type) - - msg:addByte(type) - if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1], "sendShowStoreOffers - offer.icons[1]") - elseif type == GameStore.ConverType.SHOW_MOUNT then - local mount = Mount(offer.id) - msg:addU16(mount:getClientId()) - - tryOnType = 1 - elseif type == GameStore.ConverType.SHOW_ITEM then - msg:addU16(offer.itemtype) - elseif type == GameStore.ConverType.SHOW_OUTFIT then - msg:addU16(player:getSex() == PLAYERSEX_FEMALE and offer.sexId.female or offer.sexId.male) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - - tryOnType = 1 - elseif type == GameStore.ConverType.SHOW_HIRELING then - if player:getSex() == PLAYERSEX_MALE then - msg:addByte(1) - else - msg:addByte(2) - end - msg:addU16(offer.sexId.male) - msg:addU16(offer.sexId.female) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - end - - msg:addByte(tryOnType) -- TryOn Type - msg:addU16(0) -- Collection (to-do) - msg:addU16(0) -- Popularity Score (to-do) - msg:addU32(0) -- State New Until (timestamp) - - local configure = useOfferConfigure(offer.type) - if configure == GameStore.ConfigureOffers.SHOW_CONFIGURE then - msg:addByte(1) - else - msg:addByte(0) - end - - msg:addU16(0) -- Products Capacity (unnused) - end - end - - if category.name == "Search" then - msg:addByte(0) -- Too many search results - end - - player:sendButtonIndication(haveSaleOffer, 1) - msg:sendToPlayer(player) - msg:delete() -end - -function sendShowStoreOffersOnOldProtocol(playerId, category) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - local haveSaleOffer = 0 - msg:addByte(GameStore.SendingPackets.S_StoreOffers) - msg:addString(category.name, "sendShowStoreOffersOnOldProtocol - category.name") - - if not category.offers then - msg:addU16(0) - msg:sendToPlayer(player) - player:sendButtonIndication(haveSaleOffer, 1) - return - end - - local limit = 30 - local count = 0 - for _, offer in ipairs(category.offers) do - if limit > 0 then - -- Blocking offers that are not on coin currency. On old protocol we cannot change or validate any currency instead the default (Coin) - if not offer.coinType or offer.coinType == GameStore.CoinType.Coin then - count = count + 1 - end - limit = limit - 1 - end - end - - msg:addU16(count) - for _, offer in ipairs(category.offers) do - if count > 0 and offer.coinType == GameStore.CoinType.Coin then - count = count - 1 - local name = "" - if offer.type == GameStore.OfferTypes.OFFER_TYPE_ITEM and offer.count then - name = offer.count .. "x " - end - - if offer.type == GameStore.OfferTypes.OFFER_TYPE_STACKABLE and offer.count then - name = offer.count .. "x " - end - - name = name .. (offer.name or "Something Special") - local newPrice = nil - if offer.state == GameStore.States.STATE_SALE then - local daySub = offer.validUntil - os.sdate("*t").day - if daySub < 0 then - newPrice = offer.basePrice - end - end - - local disabled, disabledReason = player:canBuyOffer(offer).disabled, player:canBuyOffer(offer).disabledReason - local offerPrice = offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] or (newPrice or offer.price or 0xFFFF) - msg:addU32(offer.id and offer.id or 0xFFFF) - msg:addString(name, "sendShowStoreOffersOnOldProtocol - name") - msg:addString(offer.description or GameStore.getDefaultDescription(offer.type, offer.count), "sendShowStoreOffersOnOldProtocol - offer.description or GameStore.getDefaultDescription(offer.type, offer.count)") - msg:addU32(offerPrice) - if offer.state then - if offer.state == GameStore.States.STATE_SALE then - local daySub = offer.validUntil - os.sdate("*t").day - if daySub >= 0 then - msg:addByte(offer.state) - msg:addU32(os.stime() + daySub * 86400) - msg:addU32(offer.basePrice) - haveSaleOffer = 1 - else - msg:addByte(GameStore.States.STATE_NONE) - end - else - msg:addByte(offer.state) - end - else - msg:addByte(GameStore.States.STATE_NONE) - end - - msg:addByte(disabled) - if disabled == 1 then - msg:addString(disabledReason, "sendShowStoreOffersOnOldProtocol - disabledReason") - end - - if offer.type == GameStore.OfferTypes.OFFER_TYPE_MOUNT then - msg:addByte(1) - msg:addString((offer.name):gsub("% ", "_") .. ".png", "sendShowStoreOffersOnOldProtocol - (offer.name).png") - elseif offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT then - msg:addByte(2) - msg:addString(offer.icons[1], "sendShowStoreOffersOnOldProtocol - offer.icons[1]") - msg:addString(offer.icons[2], "sendShowStoreOffersOnOldProtocol - offer.icons[2]") - else - msg:addByte(#offer.icons) - for k, icon in ipairs(offer.icons) do - msg:addString(icon, "sendShowStoreOffersOnOldProtocol - icon") - end - end - - msg:addU16(0) -- Suboffers - end - end - - player:sendButtonIndication(haveSaleOffer, 1) - msg:sendToPlayer(player) -end - -function sendStoreTransactionHistory(playerId, page, entriesPerPage) - local player = Player(playerId) - if not player then - return false - end - - local entries = GameStore.retrieveHistoryEntries(player:getAccountId(), page, entriesPerPage) -- this makes everything easy! - if #entries == 0 then - return addPlayerEvent(sendStoreError, 250, playerId, GameStore.StoreErrors.STORE_ERROR_HISTORY, "You don't have any entries yet.") - end - - local oldProtocol = player:getClient().version < 1200 - local totalEntries = GameStore.retrieveHistoryTotalPages(player:getAccountId()) - local totalPages = math.ceil(totalEntries / entriesPerPage) - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_OpenTransactionHistory) - msg:addU32(totalPages > 0 and page - 1 or 0x0) -- current page - msg:addU32(totalPages > 0 and totalPages or 0x0) -- total page - msg:addByte(#entries) - - for k, entry in ipairs(entries) do - if not oldProtocol then - msg:addU32(0) - end - msg:addU32(entry.time) - msg:addByte(entry.mode) -- 0 = normal, 1 = gift, 2 = refund - msg:add32(entry.amount) - if not oldProtocol then - msg:addByte(entry.type or 0x00) -- 0 = transferable tibia coin, 1 = normal tibia coin - end - msg:addString(entry.description, "sendStoreTransactionHistory - entry.description") - if not oldProtocol then - msg:addByte(0) -- details - end - end - msg:sendToPlayer(player) -end - -function sendStorePurchaseSuccessful(playerId, message) - local player = Player(playerId) - if not player then - return false - end - - local oldProtocol = player:getClient().version < 1200 - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CompletePurchase) - msg:addByte(0x00) - msg:addString(message, "sendStorePurchaseSuccessful - message") - if oldProtocol then - -- Send all coins can be used for buy store offers - msg:addU32(player:getTibiaCoins()) - -- Send transferable coins can be used on transfer - msg:addU32(player:getTransferableCoins()) - end - - msg:sendToPlayer(player) -end - -function sendStoreError(playerId, errorType, message) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_StoreError) - msg:addByte(errorType) - msg:addString(message, "sendStoreError - message") - msg:sendToPlayer(player) -end - -function sendStoreBalanceUpdating(playerId, updating) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CoinBalanceUpdating) - msg:addByte(0x00) - msg:sendToPlayer(player) - - if updating then - sendUpdatedStoreBalances(playerId) - end -end - -function sendUpdatedStoreBalances(playerId) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_CoinBalanceUpdating) - msg:addByte(0x01) - - msg:addByte(GameStore.SendingPackets.S_CoinBalance) - msg:addByte(0x01) - - -- Send total of coins (transferable and normal coin) - msg:addU32(player:getTibiaCoins()) - msg:addU32(player:getTransferableCoins()) -- How many are Transferable - - local oldProtocol = player:getClient().version < 1200 - if not oldProtocol then - -- How many are reserved for a Character Auction - -- We currently do not have this system implemented, so we will send 0 - msg:addU32(0) - end - - msg:sendToPlayer(player) -end - -function sendRequestPurchaseData(playerId, offerId, type) - local player = Player(playerId) - if not player then - return false - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_RequestPurchaseData) - msg:addU32(offerId) - msg:addByte(type) - msg:sendToPlayer(player) -end - ---==GameStoreFunctions==-- -GameStore.getCategoryByName = function(name) - for k, category in ipairs(GameStore.Categories) do - if category.name:lower() == name:lower() then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - end - return nil -end - -GameStore.getCategoryByOffer = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if type(offer.id) == "number" then - if offer.id == id then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - elseif type(offer.id) == "table" then - for m, offerId in pairs(offer.id) do - -- in case of outfits we have offer.id = {male = ..., female = ...} - if offerId == id then - if not category.offers then - return GameStore.getCategoryByName(category.subclasses[1]) - end - return category - end - end - end - end - end - end - return nil -end - -GameStore.getOfferById = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if type(offer.id) == "number" then - if offer.id == id then - return offer - end - elseif type(offer.id) == "table" and (offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT or offer.type == GameStore.OfferTypes.OFFER_TYPE_OUTFIT_ADDON) then - for m, offerId in pairs(offer.id) do - -- in case of outfits we have offer.id = {male = ..., female = ...} - if offerId == id then - return offer - end - end - - -- case multi offer - elseif type(offer.id) == "table" then - local newoffer = offer - for i = 1, #offer.id do - local offerId = offer.id[i] - if offerId == id then - newoffer.id = offerId - newoffer.price = offer.price[i] - return newoffer - end - end - end - end - end - end - return nil -end - --- Using for multi offer -function GameStore.getOffersByName(name) - local offers = {} - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers then - for Off_k, offer in ipairs(category.offers) do - if offer.name:lower() == name:lower() then - table.insert(offers, offer) - end - end - end - end - return offers -end - -GameStore.haveCategoryRook = function() - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers and category.rookgaard then - return true - end - end - - return false -end - -GameStore.haveOfferRook = function(id) - for Cat_k, category in ipairs(GameStore.Categories) do - if category.offers and category.rookgaard then - for Off_k, offer in ipairs(category.offers) do - if offer.id == id then - return true - end - end - end - end - return nil -end - -GameStore.insertHistory = function(accountId, mode, description, coinAmount, coinType) - return db.query(string.format("INSERT INTO `store_history`(`account_id`, `mode`, `description`, `coin_type`, `coin_amount`, `time`) VALUES (%s, %s, %s, %s, %s, %s)", accountId, mode, db.escapeString(description), coinType, coinAmount, os.time())) -end - -GameStore.retrieveHistoryTotalPages = function(accountId) - local resultId = db.storeQuery("SELECT count(id) as total FROM store_history WHERE account_id = " .. accountId) - if not resultId then - return 0 - end - - local totalPages = Result.getNumber(resultId, "total") - Result.free(resultId) - return totalPages -end - -GameStore.retrieveHistoryEntries = function(accountId, currentPage, entriesPerPage) - local entries = {} - local offset = currentPage > 1 and entriesPerPage * (currentPage - 1) or 0 - - local resultId = db.storeQuery("SELECT * FROM `store_history` WHERE `account_id` = " .. accountId .. " ORDER BY `time` DESC LIMIT " .. offset .. ", " .. entriesPerPage .. ";") - if resultId then - repeat - local entry = { - mode = Result.getNumber(resultId, "mode"), - description = Result.getString(resultId, "description"), - amount = Result.getNumber(resultId, "coin_amount"), - type = Result.getNumber(resultId, "coin_type"), - time = Result.getNumber(resultId, "time"), - } - table.insert(entries, entry) - until not Result.next(resultId) - Result.free(resultId) - end - return entries -end - -GameStore.getDefaultDescription = function(offerType, count) - local t, descList = GameStore.OfferTypes - if offerType == t.OFFER_TYPE_OUTFIT or offerType == t.OFFER_TYPE_OUTFIT_ADDON then - descList = GameStore.DefaultDescriptions.OUTFIT - elseif offerType == t.OFFER_TYPE_MOUNT then - descList = GameStore.DefaultDescriptions.MOUNT - elseif offerType == t.OFFER_TYPE_NAMECHANGE then - descList = GameStore.DefaultDescriptions.NAMECHANGE - elseif offerType == t.OFFER_TYPE_SEXCHANGE then - descList = GameStore.DefaultDescriptions.SEXCHANGE - elseif offerType == t.OFFER_TYPE_EXPBOOST then - descList = GameStore.DefaultDescriptions.EXPBOOST - elseif offerType == t.OFFER_TYPE_PREYSLOT then - descList = GameStore.DefaultDescriptions.PREYSLOT - elseif offerType == t.OFFER_TYPE_PREYBONUS then - descList = GameStore.DefaultDescriptions.PREYBONUS - elseif offerType == t.OFFER_TYPE_TEMPLE then - descList = GameStore.DefaultDescriptions.TEMPLE - end - - return descList[math.floor(math.random(1, #descList))] or "" -end - -GameStore.canUseHirelingName = function(name) - local result = { - ability = false, - } - if name:len() < 3 or name:len() > 18 then - result.reason = "The length of the hireling name must be between 3 and 18 characters." - return result - end - - local match = name:gmatch("%s+") - local count = 0 - for v in match do - count = count + 1 - end - - local matchtwo = name:match("^%s+") - if matchtwo then - result.reason = "The hireling name can't have whitespace at begin." - return result - end - - local matchthree = name:match("[^a-zA-Z ]") - if matchthree then - result.reason = "The hireling name has invalid characters" - return result - end - - if count > 1 then - result.reason = "The hireling name have more than 1 whitespace." - return result - end - - -- just copied from znote aac. - local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } - local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do - if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. '" in your hireling name.' - return result - end - end - end - - local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if tmpName:lower():find(words[i]) then - result.reason = "You can't use word \"" .. words[i] .. '" with whitespace in your hireling name.' - return result - end - end - - result.ability = true - return result -end - -GameStore.canChangeToName = function(name) - local result = { - ability = false, - } - if name:len() < 3 or name:len() > 18 then - result.reason = "The length of your new name must be between 3 and 18 characters." - return result - end - - local match = name:gmatch("%s+") - local count = 0 - for v in match do - count = count + 1 - end - - local matchtwo = name:match("^%s+") - if matchtwo then - result.reason = "Your new name can't have whitespace at begin." - return result - end - - if count > 1 then - result.reason = "Your new name have more than 1 whitespace." - return result - end - - -- just copied from znote aac. - local words = { "owner", "gamemaster", "hoster", "admin", "staff", "tibia", "account", "god", "anal", "ass", "fuck", "sex", "hitler", "pussy", "dick", "rape", "adm", "cm", "gm", "tutor", "counsellor" } - local split = name:split(" ") - for k, word in ipairs(words) do - for k, nameWord in ipairs(split) do - if nameWord:lower() == word then - result.reason = "You can't use word \"" .. word .. '" in your new name.' - return result - end - end - end - - local tmpName = name:gsub("%s+", "") - for i = 1, #words do - if tmpName:lower():find(words[i]) then - result.reason = "You can't use word \"" .. words[i] .. '" with whitespace in your new name.' - return result - end - end - - if MonsterType(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a monster's name." - return result - elseif Npc(name) then - result.reason = 'Your new name "' .. name .. "\" can't be a npc's name." - return result - end - - local letters = "{}|_*+-=<>0123456789@#%^&()/*'\\.,:;~!\"$" - for i = 1, letters:len() do - local c = letters:sub(i, i) - for i = 1, name:len() do - local m = name:sub(i, i) - if m == c then - result.reason = "You can't use this letter \"" .. c .. '" in your new name.' - return result - end - end - end - result.ability = true - return result -end - --- --- PURCHASE PROCESSOR FUNCTIONS --- Must throw an error when the purchase has not been made. The error must of --- take a table {code = ..., message = ...} if the error is handled. When no code --- index is present the error is assumed to be unhandled. - -function GameStore.processItemPurchase(player, offerId, offerCount, movable, setOwner) - local canReceive, errorMsg = player:canReceiveStoreItems(offerId, offerCount) - if not canReceive then - return error({ code = 0, message = errorMsg }) - end - - for t = 1, offerCount do - player:addItemStoreInbox(offerId, offerCount or 1, movable, setOwner) - end -end - -function GameStore.processChargesPurchase(player, offerId, name, charges, movable, setOwner) - local canReceive, errorMsg = player:canReceiveStoreItems(offerId, 1) - if not canReceive then - return error({ code = 0, message = errorMsg }) - end - - player:addItemStoreInbox(offerId, charges, movable, setOwner) -end - -function GameStore.processSingleBlessingPurchase(player, blessId, count) - player:addBlessing(blessId, count) -end - -function GameStore.processAllBlessingsPurchase(player, count) - player:addBlessing(1, count) - player:addBlessing(2, count) - player:addBlessing(3, count) - player:addBlessing(4, count) - player:addBlessing(5, count) - player:addBlessing(6, count) - player:addBlessing(7, count) - player:addBlessing(8, count) -end - -function GameStore.processInstantRewardAccess(player, offerCount) - local limit = GameStore.ItemLimit.INSTANT_REWARD_ACCESS - if player:getCollectionTokens() + offerCount >= limit + 1 then - return error({ code = 1, message = "You cannot own more than " .. limit .. " reward tokens." }) - end - player:setCollectionTokens(player:getCollectionTokens() + offerCount) -end - -function GameStore.processCharmsPurchase(player) - player:charmExpansion(true) -end - -function GameStore.processPremiumPurchase(player, offerId) - player:addPremiumDays(offerId - 3000) - if configManager.getBoolean(configKeys.VIP_SYSTEM_ENABLED) then - player:onAddVip(offerId - 3000) - end -end - -function GameStore.processStackablePurchase(player, offerId, offerCount, offerName, movable, setOwner) - local canReceive, errorMsg = player:canReceiveStoreItems(offerId, offerCount) - if not canReceive then - return error({ code = 0, message = errorMsg }) - end - - local iType = ItemType(offerId) - if not iType then - return nil - end - - local inbox = player:getStoreInbox() - if inbox then - local stackSize = iType:getStackSize() - local remainingCount = offerCount - while remainingCount > 0 do - local countToAdd = math.min(remainingCount, stackSize) - local inboxItem = inbox:addItem(offerId, countToAdd) - if inboxItem then - if not movable then - inboxItem:setAttribute(ITEM_ATTRIBUTE_STORE, systemTime()) - end - else - return error({ code = 0, message = "Error adding item to store inbox." }) - end - remainingCount = remainingCount - countToAdd - end - end -end - -function GameStore.processHouseRelatedPurchase(player, offer) - local function isCaskItem(itemId) - return (itemId >= ITEM_HEALTH_CASK_START and itemId <= ITEM_HEALTH_CASK_END) or (itemId >= ITEM_MANA_CASK_START and itemId <= ITEM_MANA_CASK_END) or (itemId >= ITEM_SPIRIT_CASK_START and itemId <= ITEM_SPIRIT_CASK_END) - end - - local itemIds = offer.itemtype - if type(itemIds) ~= "table" then - itemIds = { itemIds } - end - - local canReceive, errorMsg = player:canReceiveStoreItems(#itemIds) - if not canReceive then - return error({ code = 0, message = errorMsg }) - end - - local inbox = player:getStoreInbox() - if inbox then - for _, itemId in ipairs(itemIds) do - if isCaskItem(itemId) then - local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) - if decoKit then - decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item in the Store.\nUnwrap it in your own house to create a <" .. ItemType(itemId):getName() .. ">.") - decoKit:setCustomAttribute("unWrapId", itemId) - decoKit:setAttribute(ITEM_ATTRIBUTE_DATE, offer.count) - - if not offer.movable then - decoKit:setAttribute(ITEM_ATTRIBUTE_STORE, systemTime()) - end - end - else - for i = 1, offer.count do - local decoKit = inbox:addItem(ITEM_DECORATION_KIT, 1) - if decoKit then - decoKit:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "You bought this item in the Store.\nUnwrap it in your own house to create a <" .. ItemType(itemId):getName() .. ">.") - decoKit:setCustomAttribute("unWrapId", itemId) - - if not offer.movable then - decoKit:setAttribute(ITEM_ATTRIBUTE_STORE, systemTime()) - end - end - end - end - end - player:sendUpdateContainer(inbox) - end -end - -function GameStore.processOutfitPurchase(player, offerSexIdTable, addon) - local looktype - local _addon = addon and addon or 0 - - if player:getSex() == PLAYERSEX_MALE then - looktype = offerSexIdTable.male - elseif player:getSex() == PLAYERSEX_FEMALE then - looktype = offerSexIdTable.female - end - - if not looktype then - return error({ code = 0, message = "This outfit seems not to suit your sex, we are sorry for that!" }) - elseif (not player:hasOutfit(looktype, 0)) and (_addon == 1 or _addon == 2) then - return error({ code = 0, message = "You must own the outfit before you can buy its addon." }) - elseif player:hasOutfit(looktype, _addon) then - return error({ code = 0, message = "You already own this outfit." }) - else - if not player:addOutfitAddon(looktype, _addon) or not player:hasOutfit(looktype, _addon) then - error({ code = 0, message = "There has been an issue with your outfit purchase. Your purchase has been cancelled." }) - else - player:addOutfitAddon(offerSexIdTable.male, _addon) - player:addOutfitAddon(offerSexIdTable.female, _addon) - end - end -end - -function GameStore.processMountPurchase(player, offerId) - if player:hasMount(offerId) then - return error({ code = 0, message = "You already own this mount." }) - end - - player:addMount(offerId) -end - -function GameStore.processNameChangePurchase(player, offer, productType, newName) - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE then - local tile = Tile(player:getPosition()) - if tile then - if not tile:hasFlag(TILESTATE_PROTECTIONZONE) then - return error({ code = 1, message = "You can change name only in Protection Zone." }) - end - end - - newName = newName:lower():trim():gsub("(%l)(%w*)", function(a, b) - return string.upper(a) .. b - end) - - local normalizedName = Game.getNormalizedPlayerName(newName, true) - if normalizedName then - return error({ code = 1, message = "This name is already used, please try again!" }) - end - - local result = GameStore.canChangeToName(newName) - if not result.ability then - return error({ code = 1, message = result.reason }) - end - - local message, namelockReason = "", player:kv():get("namelock") - if not namelockReason then - player:makeCoinTransaction(offer) - message = string.format("You have purchased %s for %d coins.", offer.name, offer.price) - else - message = "Your character has been renamed successfully." - end - addPlayerEvent(sendStorePurchaseSuccessful, 500, player:getId(), message) - - player:changeName(newName) - else - return addPlayerEvent(sendRequestPurchaseData, 250, player:getId(), offer.id, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE) - end -end - -function GameStore.processSexChangePurchase(player) - player:toggleSex() -end - -function GameStore.processExpBoostPurchase(player) - local currentXpBoostTime = player:getXpBoostTime() - local expBoostCount = player:getStorageValue(GameStore.Storages.expBoostCount) - - player:setXpBoostPercent(50) - player:setXpBoostTime(currentXpBoostTime + 3600) - - if expBoostCount == -1 or expBoostCount == 6 then - expBoostCount = 1 - end - - player:setStorageValue(GameStore.Storages.expBoostCount, expBoostCount + 1) -end - -function GameStore.processPreyThirdSlot(player) - if player:preyThirdSlot() then - return error({ code = 1, message = "You already have unlocked all prey slots." }) - end - player:preyThirdSlot(true) -end - -function GameStore.processTaskHuntingThirdSlot(player) - if player:taskHuntingThirdSlot() then - return error({ code = 1, message = "You already have unlocked all task hunting slots." }) - end - player:taskHuntingThirdSlot(true) -end - -function GameStore.processPreyBonusReroll(player, offerCount) - local limit = GameStore.ItemLimit.PREY_WILDCARD - if player:getPreyCards() + offerCount >= limit + 1 then - return error({ code = 1, message = "You cannot own more than " .. limit .. " prey wildcards." }) - end - player:addPreyCards(offerCount) -end - -function GameStore.processTempleTeleportPurchase(player) - local inPz = player:getTile():hasFlag(TILESTATE_PROTECTIONZONE) - local inFight = player:isPzLocked() or player:getCondition(CONDITION_INFIGHT, CONDITIONID_DEFAULT) - if not inPz and inFight then - return error({ code = 0, message = "You can't use temple teleport in fight!" }) - end - - player:teleportTo(player:getTown():getTemplePosition()) - player:getPosition():sendMagicEffect(CONST_ME_TELEPORT) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have been teleported to your hometown.") -end - -function GameStore.processHirelingPurchase(player, offer, productType, hirelingName, chosenSex) - if player:getClient().version < 1200 then - return error({ code = 1, message = "You cannot buy hirelings on client 10, please relog on client 12 and try again." }) - end - - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_HIRELING then - local result = GameStore.canUseHirelingName(hirelingName) - if not result.ability then - return error({ code = 1, message = result.reason }) - end - - hirelingName = hirelingName:lower():gsub("(%l)(%w*)", function(a, b) - return string.upper(a) .. b - end) - - local hireling = player:addNewHireling(hirelingName, chosenSex) - if not hireling then - return error({ code = 1, message = "Error delivering your hireling lamp, try again later." }) - end - - player:makeCoinTransaction(offer, hirelingName) - local message = "You have successfully bought " .. hirelingName - player:createTransactionSummary(offer.type, 1) - return addPlayerEvent(sendStorePurchaseSuccessful, 650, player:getId(), message) - -- If not, we ask him to do! - else - if player:getHirelingsCount() >= GameStore.ItemLimit.HIRELING then - return error({ code = 1, message = "You cannot have more than " .. GameStore.ItemLimit.HIRELING .. " hirelings." }) - end - -- TODO: Use the correct dialog (byte 0xDB) on client 1205+ - -- for compatibility, request name using the change name dialog - return addPlayerEvent(sendRequestPurchaseData, 250, player:getId(), offer.id, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_HIRELING) - end -end - --- Hireling Helpers -local function HandleHirelingNameChange(playerId, offer, newHirelingName) - local player = Player(playerId) - if not player then - return - end - - local functionCallback = function(playerIdInFunction, data, hireling) - local playerInFunction = Player(playerIdInFunction) - if not playerInFunction then - return - end - - if not hireling then - return playerInFunction:showInfoModal("Error", "Your must select a hireling.") - end - - if hireling.active > 0 then - return playerInFunction:showInfoModal("Error", "Your hireling must be inside his/her lamp.") - end - - local oldName = hireling.name - hireling.name = data.newHirelingName - - if not playerInFunction:makeCoinTransaction(data.offer, oldName .. " to " .. hireling.name) then - return playerInFunction:showInfoModal("Error", "Transaction error") - end - - local lamp = playerInFunction:findHirelingLamp(hireling:getId()) - if lamp then - lamp:setAttribute(ITEM_ATTRIBUTE_DESCRIPTION, "This mysterious lamp summons your very own personal hireling.\nThis item cannot be traded.\nThis magic lamp is the home of " .. hireling:getName() .. ".") - end - logger.debug("{} has been renamed to {}", oldName, hireling.name) - sendUpdatedStoreBalances(playerIdInFunction) - end - - player:sendHirelingSelectionModal("Choose a Hireling", "Select a hireling below", functionCallback, { offer = offer, newHirelingName = newHirelingName }) -end - -function GameStore.processHirelingChangeNamePurchase(player, offer, productType, newHirelingName) - if player:getClient().version < 1200 then - return error({ - code = 1, - message = "You cannot buy hireling change name on client 10, please relog on client 12 and try again.", - }) - end - - if productType == GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE then - local result = GameStore.canUseHirelingName(newHirelingName) - if not result.ability then - return error({ code = 1, message = result.reason }) - end - - newHirelingName = newHirelingName:lower():gsub("(%l)(%w*)", function(a, b) - return string.upper(a) .. b - end) - - local message = "Close the store window to select which hireling should be renamed to " .. newHirelingName - local playerId = player:getId() - addPlayerEvent(sendStorePurchaseSuccessful, 200, playerId, message) - addPlayerEvent(HandleHirelingNameChange, 550, playerId, offer, newHirelingName) - else - return addPlayerEvent(sendRequestPurchaseData, 250, player:getId(), offer.id, GameStore.ClientOfferTypes.CLIENT_STORE_OFFER_NAMECHANGE) - end -end - -local function HandleHirelingSexChange(playerId, offer) - local player = Player(playerId) - if not player then - return - end - - local functionCallback = function(playerIdInFunction, data, hireling) - local playerInFunction = Player(playerIdInFunction) - if not playerInFunction then - return - end - - if not hireling then - return playerInFunction:showInfoModal("Error", "Your must select a hireling.") - end - - if hireling.active > 0 then - return playerInFunction:showInfoModal("Error", "Your hireling must be inside his/her lamp.") - end - - if not playerInFunction:makeCoinTransaction(data.offer, hireling:getName()) then - return playerInFunction:showInfoModal("Error", "Transaction error") - end - - local changeTo, sexString, lookType - if hireling.sex == HIRELING_SEX.FEMALE then - changeTo = HIRELING_SEX.MALE - sexString = "male" - lookType = HIRELING_OUTFIT_DEFAULT.male - else - changeTo = HIRELING_SEX.FEMALE - sexString = "female" - lookType = HIRELING_OUTFIT_DEFAULT.female - end - - hireling.sex = changeTo - hireling.looktype = lookType - - logger.debug("{} sex was changed to {}", hireling:getName(), sexString) - sendUpdatedStoreBalances(playerIdInFunction) - end - - player:sendHirelingSelectionModal("Choose a Hireling", "Select a hireling below", functionCallback, { offer = offer }) -end - -function GameStore.processHirelingChangeSexPurchase(player, offer) - if player:getClient().version < 1200 then - return error({ - code = 1, - message = "You cannot buy hireling change sex on client 10, please relog on client 12 and try again.", - }) - end - - local message = "Close the store window to select which hireling should have the sex changed." - local playerId = player:getId() - addPlayerEvent(sendStorePurchaseSuccessful, 200, playerId, message) - addPlayerEvent(HandleHirelingSexChange, 550, playerId, offer) -end - -function GameStore.processHirelingSkillPurchase(player, offer) - if player:getClient().version < 1200 then - return error({ - code = 1, - message = "You cannot buy hireling skill on client 10, please relog on client 12 and try again.", - }) - end - - player:getPosition():sendMagicEffect(CONST_ME_MAGIC_BLUE) - player:enableHirelingSkill(GetHirelingSkillNameById(offer.id)) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "A new hireling skill has been added to all your hirelings") -end - -function GameStore.processHirelingOutfitPurchase(player, offer) - if player:getClient().version < 1200 then - return error({ - code = 1, - message = "You cannot buy hireling outfit on client 10, please relog on client 12 and try again.", - }) - end - - local outfitName = GetHirelingOutfitNameById(offer.id) - logger.debug("Processing hireling outfit purchase name {}", outfitName) - player:getPosition():sendMagicEffect(CONST_ME_MAGIC_GREEN) - player:enableHirelingOutfit(outfitName) - player:sendTextMessage(MESSAGE_EVENT_ADVANCE, "A new hireling outfit has been added to all your hirelings") -end - ---==Player==-- --- Character auction coins -function Player.canRemoveCoins(self, coins) - return self:getTibiaCoins() >= coins -end - -function Player.removeCoinsBalance(self, coins) - if self:canRemoveCoins(coins) then - sendStoreBalanceUpdating(self:getId(), true) - self:removeTibiaCoins(coins) - return true - end - - return false -end - -function Player.addCoinsBalance(self, coins, update) - self:addTibiaCoins(coins) - if update then - sendStoreBalanceUpdating(self:getId(), true) - end - return true -end - --- Transferable coins -function Player.canRemoveTransferableCoins(self, coins) - return self:getTransferableCoins() >= coins -end - -function Player.removeTransferableCoinsBalance(self, coins) - if self:canRemoveTransferableCoins(coins) then - sendStoreBalanceUpdating(self:getId(), true) - self:removeTransferableCoins(coins) - return true - end - - return false -end - -function Player.addTransferableCoinsBalance(self, coins, update) - self:addTransferableCoins(coins) - if update then - sendStoreBalanceUpdating(self:getId(), true) - end - return true -end - ---- Support Functions -function Player.makeCoinTransaction(self, offer, desc) - local op = false - - if desc then - desc = offer.name .. " (" .. desc .. ")" - else - desc = offer.name - end - - if offer.coinType == GameStore.CoinType.Coin and self:canRemoveCoins(offer.price) then - op = self:removeCoinsBalance(offer.price) - elseif offer.coinType == GameStore.CoinType.Transferable and self:canRemoveTransferableCoins(offer.price) then - op = self:removeTransferableCoinsBalance(offer.price) - end - - -- When the transaction is successful add to the history - if op then - GameStore.insertHistory(self:getAccountId(), GameStore.HistoryTypes.HISTORY_TYPE_NONE, desc, offer.price * -1, offer.coinType) - end - - return op -end - --- Verifies if the player has enough resources to afford a given offer. --- @param coinsToRemove (number) - The amount of coins required for the offer. --- @param coinType (string) - The type of the offer. --- @return (boolean) - Returns true if the player can pay for the offer, false otherwise. -function Player.canPayForOffer(self, coinsToRemove, coinType) - -- Check if the player has the required amount of regular coins and the offer type is regular. - if coinType == GameStore.CoinType.Coin then - return self:canRemoveCoins(coinsToRemove) - end - - -- Check if the player has the required amount of transferable coins and the offer type is transferable. - if coinType == GameStore.CoinType.Transferable then - return self:canRemoveTransferableCoins(coinsToRemove) - end - - return false -end - ---- Other players functions - -function Player.sendButtonIndication(self, value1, value2) - local msg = NetworkMessage() - msg:addByte(0x19) - msg:addByte(value1) -- Sale - msg:addByte(value2) -- New Item - msg:sendToPlayer(self) -end - -function Player.toggleSex(self) - local currentSex = self:getSex() - local playerOutfit = self:getOutfit() - - playerOutfit.lookAddons = 0 - if currentSex == PLAYERSEX_FEMALE then - self:setSex(PLAYERSEX_MALE) - playerOutfit.lookType = 128 - else - self:setSex(PLAYERSEX_FEMALE) - playerOutfit.lookType = 136 - end - self:setOutfit(playerOutfit) -end - -local function getHomeOffers(playerId) - local player = Player(playerId) - if not player then - return {} - end - - local GameStoreCategories = GameStore.Categories - - local offers = {} - if GameStoreCategories then - for k, category in ipairs(GameStoreCategories) do - if category.offers then - for _, offer in ipairs(category.offers) do - if offer.home then - table.insert(offers, offer) - end - end - end - end - end - - return offers -end - -function sendHomePage(playerId) - local player = Player(playerId) - if not player then - return - end - - local msg = NetworkMessage() - msg:addByte(GameStore.SendingPackets.S_StoreOffers) - - msg:addString("Home", "sendHomePage - Home") - msg:addU32(0x0) -- Redirect ID (not used here) - msg:addByte(0x0) -- Window Type - msg:addByte(0x0) -- Collections Size - msg:addU16(0x00) -- Collection Name - - local disableReasons = {} - local homeOffers = getHomeOffers(player:getId()) - for p, offer in pairs(homeOffers) do - local canBuy = player:canBuyOffer(offer) - if canBuy.disabled == 1 then - for index, disableTable in ipairs(disableReasons) do - if canBuy.disabledReason == disableTable.reason then - offer.disabledReadonIndex = index - end - end - - if offer.disabledReadonIndex == nil then - offer.disabledReadonIndex = #disableReasons - table.insert(disableReasons, canBuy.disabledReason) - end - end - end - - msg:addU16(#disableReasons) - for _, reason in ipairs(disableReasons) do - msg:addString(reason, "sendHomePage - reason") - end - - msg:addU16(#homeOffers) -- offers - for p, offer in pairs(homeOffers) do - local offerPrice = offer.type == GameStore.OfferTypes.OFFER_TYPE_EXPBOOST and GameStore.ExpBoostValues[player:getStorageValue(GameStore.Storages.expBoostCount)] or offer.price - if offer.type == GameStore.OfferTypes.OFFER_TYPE_NAMECHANGE and player:kv():get("namelock") then - offerPrice = 0 - end - - msg:addString(offer.name, "sendHomePage - offer.name") - msg:addByte(0x1) -- ? - msg:addU32(offer.id or 0) -- id - msg:addU16(0x1) - msg:addU32(offerPrice) - msg:addByte(offer.coinType or 0x00) - - msg:addByte((offer.disabledReadonIndex ~= nil) and 1 or 0) - if offer.disabledReadonIndex ~= nil then - msg:addByte(0x01) - msg:addU16(offer.disabledReadonIndex) - offer.disabledReadonIndex = nil -- Reseting the table to nil disable reason - end - - msg:addByte(0x00) - - local type = convertType(offer.type) - - msg:addByte(type) - if type == GameStore.ConverType.SHOW_NONE then - msg:addString(offer.icons[1], "sendHomePage - offer.icons[1]") - elseif type == GameStore.ConverType.SHOW_MOUNT then - local mount = Mount(offer.id) - if not mount then - msg:addU16(0) - else - msg:addU16(mount:getClientId()) - end - elseif type == GameStore.ConverType.SHOW_ITEM then - msg:addU16(offer.itemtype) - elseif type == GameStore.ConverType.SHOW_OUTFIT then - msg:addU16(player:getSex() == PLAYERSEX_FEMALE and offer.sexId.female or offer.sexId.male) - local outfit = player:getOutfit() - msg:addByte(outfit.lookHead) - msg:addByte(outfit.lookBody) - msg:addByte(outfit.lookLegs) - msg:addByte(outfit.lookFeet) - end - - msg:addByte(0) -- TryOn Type - msg:addU16(0) -- Collection - msg:addU16(0) -- Popularity Score - msg:addU32(0) -- State New Until - msg:addByte(0) -- User Configuration - msg:addU16(0) -- Products Capacity - end - - local banner = HomeBanners - msg:addByte(#banner.images) - for m, image in ipairs(banner.images) do - msg:addString(image, "sendHomePage - image") - msg:addByte(0x04) -- Banner Type (offer) - msg:addU32(0x00) -- Offer Id - msg:addByte(0) - msg:addByte(0) - end - - msg:addByte(banner.delay) -- Delay to swtich images - - msg:sendToPlayer(player) -end - ---exporting the method so other scripts can use to open store -function Player:openStore(serviceName) - local playerId = self:getId() - openStore(playerId) - - --local serviceType = msg:getByte() - local category = GameStore.Categories and GameStore.Categories[1] or nil - - if serviceName and serviceName:lower() == "home" then - return sendHomePage(playerId) - end - - if serviceName and GameStore.getCategoryByName(serviceName) then - category = GameStore.getCategoryByName(serviceName) - end - - if category then - addPlayerEvent(sendShowStoreOffers, 50, playerId, category) - end -end diff --git a/data/modules/scripts/gamestore/readme.md b/data/modules/scripts/gamestore/readme.md deleted file mode 100644 index e179a1b8172..00000000000 --- a/data/modules/scripts/gamestore/readme.md +++ /dev/null @@ -1,119 +0,0 @@ -## Module : Premium Shop - Game Store ---- - -> Notify! Please put the images in folder called "64". - -### Faq - -##### 1. How to make a category? -```lua ---- Method 1 --- -GameStore.Categories = { - { name = "mounts", - ... - }, - { name = "outfits", - ... - } -} ---- Method 2 --- -GameStore.Categories = { - mounts = { - name = "mounts" - }, - outfits = {....} -} ---- Method 3 --- -mounts = {....} -GameStore.Categories = { - mounts, - ... -} -``` - -##### 2. How to add offers to category -```lua ---- Method 1 --- -GameStore.Categories = { - mounts = { - offers = { - - } - } -} ---- Method 2 --- -mounts = { name = "mounts", icons = {"Category_Mounts.png"}} -mounts.offers = { - {name = "fafa", thingId = ....} -} -GameStore.Categories = { - mounts = { name = "mounts", - offers = { - {name = "fafa", thingId = ....} - } - } -} -``` - -### Category Options -| Method | Type | Usage | Default | -|-------------|-----------------------|------------------------------|-----------------------------| -| name* | string | the category name | nil | -| description | string | the category description | "" | -| state | GameStore.States(int) | the category highlight state | GameStore.States.STATE_NONE | -| icons* | table[string(s)] | the icons for the category | nil | -| offers(*) | table[offer(s)] | the category offers | nil | - -#### Example : -```lua -mounts = { - name = "Mounts", - description = "Have a mount and become an important-look person!", - state = GameStore.States.STATE_NEW, - icons = {"Category_Mounts.png"}, - offers = {....} -} -``` - -### Offer Options -| Method | Type | Usage | Default | -|----------------|---------------------------|------------------------------------------------------------------------|--------------------------------------| -| name* | string | the offer name | nil | -| description | string | the offer descrioption | "" | -| thingId* | int | the id of the choosed type ( itemId or mountId or outfitLookType, ....)| nil | -| type* | GameStore.OfferTypes(int) | the type of the offer, item or mount or outfit or ... | GameStore.OfferTypes.OFFER_TYPE_NONE | -| price* | int | the offer price | nil | -| state | GameStore.States(int) | the offer highlight state | GameStore.States.STATE_NONE | -| icons* | table[string(s)] | the icons for the category | nil | -| disabled | bool | dynamically disable the offer | false | -| disabledReason | string | reason for being disabled ( use when disabled is true ) | nil | - -#### Example : -```lua -mounts.offers = { - { name = "Titanica", - description = "Looking for nice mount? Titanica is the one you are looking for, she is beautiful, smart and running quickly.", - thingId = 4, -- here we use mount id. - type = GameStore.OfferTypes.OFFER_TYPE_MOUNT, - price = 500, - state = GameStore.States.STATE_SALE, - icons = {"Product_Titanica1", "Product_Titanica2"} - --[[Dynamicly used variables - disabled = (true|false), - disbabledReason = (string) - ]] - } -} -``` - -### Offer types and States. - -| OfferTypes | States | -|-------------------------|-------------| -| `OFFER_TYPE_ITEM` | `STATE_NEW` | -| `OFFER_TYPE_OUTFIT` | `STATE_SALE` | -| `OFFER_TYPE_OUTFIT_ADDON` | `STATE_TIMED` | -| `OFFER_TYPE_MOUNT` | | -| `OFFER_TYPE_NAMECHANGE` | | -| `OFFER_TYPE_SEXCHANGE` | | -| `OFFER_TYPE_PROMOTION` | | diff --git a/data/scripts/globalevents/server_initialization.lua b/data/scripts/globalevents/server_initialization.lua index df29660d373..d2f9614beea 100644 --- a/data/scripts/globalevents/server_initialization.lua +++ b/data/scripts/globalevents/server_initialization.lua @@ -10,7 +10,7 @@ local function cleanupDatabase() db.query("DELETE FROM `player_storage` WHERE `key` IN (" .. Global.Storage.FamiliarSummonEvent10 .. ", " .. Global.Storage.FamiliarSummonEvent60 .. ")") db.query("UPDATE `players` SET `isreward` = " .. DAILY_REWARD_NOTCOLLECTED) - db.query("UPDATE `player_storage` SET `value` = 0 WHERE `player_storage`.`key` = 51052") + db.query("UPDATE `player_storage` SET `value` = 1 WHERE `player_storage`.`key` = 51052") end -- Function to move expired bans to ban history diff --git a/schema.sql b/schema.sql index 86ea9e1bf6b..89ffb343120 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '46'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '47'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -783,13 +783,14 @@ CREATE TABLE IF NOT EXISTS `player_storage` ( CREATE TABLE IF NOT EXISTS `store_history` ( `id` int(11) NOT NULL AUTO_INCREMENT, `account_id` int(11) UNSIGNED NOT NULL, - `mode` smallint(2) NOT NULL DEFAULT '0', `description` varchar(3500) NOT NULL, - `coin_type` tinyint(1) NOT NULL DEFAULT '0', `coin_amount` int(12) NOT NULL, - `time` bigint(20) UNSIGNED NOT NULL, - `timestamp` int(11) NOT NULL DEFAULT '0', - `coins` int(11) NOT NULL DEFAULT '0', + `coin_type` tinyint(1) NOT NULL DEFAULT '0', + `type` smallint(2) NOT NULL DEFAULT '0', + `show_detail` smallint(2) UNSIGNED NOT NULL DEFAULT '0', + `player_name` varchar(255) DEFAULT NULL, + `total_price` bigint NOT NULL DEFAULT '0', + `created_at` bigint UNSIGNED NOT NULL DEFAULT '0', INDEX `account_id` (`account_id`), CONSTRAINT `store_history_pk` PRIMARY KEY (`id`), CONSTRAINT `store_history_account_fk` diff --git a/src/account/account.cpp b/src/account/account.cpp index 93596f77b15..5c260b8d7bf 100644 --- a/src/account/account.cpp +++ b/src/account/account.cpp @@ -131,7 +131,7 @@ AccountErrors_t Account::removeCoins(CoinType type, const uint32_t &amount, cons } if (coins < amount) { - g_logger().info("Account doesn't have enough coins! current[{}], remove:[{}]", coins, amount); + g_logger().info("Account {} doesn't have enough coins! current[{}], remove:[{}]", getID(), coins, amount); return RemoveCoins; } @@ -162,6 +162,24 @@ void Account::registerCoinTransaction(CoinTransactionType transactionType, CoinT } } +void Account::registerStoreTransaction(CoinTransactionType type, const uint32_t &amount, CoinType coinType, const std::string &description, const time_t &time) { + if (!m_accLoaded) { + return; + } + + if (description.empty()) { + return; + } + + if (!g_accountRepository().registerStoreTransaction(m_account->id, type, amount, coinType, description, time)) { + g_logger().error( + "Failed to register transaction: 'account:[{}], transaction " + "type:[{}], coins:[{}], coin type:[{}], description:[{}], time:[{}]", + m_account->id, type, amount, type, description, time + ); + } +} + [[nodiscard]] uint32_t Account::getID() const { return m_account->id; }; diff --git a/src/account/account.hpp b/src/account/account.hpp index 2c6098a8dbd..06a9f524e85 100644 --- a/src/account/account.hpp +++ b/src/account/account.hpp @@ -60,6 +60,17 @@ class Account { */ void registerCoinTransaction(CoinTransactionType transactionType, CoinType type, const uint32_t &amount, const std::string &detail); + /** + * @brief Registers a store transaction. + * + * @param type Type of history entry + * @param coinType Type of the coin + * @param amount Amount of coins to be registered + * @param description Detail of the transaction + * @param time Time of the transaction + */ + void registerStoreTransaction(CoinTransactionType type, const uint32_t &amount, CoinType coinType, const std::string &description, const time_t &time); + /*************************************************************************** * Account Load/Save **************************************************************************/ diff --git a/src/account/account_repository.hpp b/src/account/account_repository.hpp index 5a9cd5292c3..509eb14fa89 100644 --- a/src/account/account_repository.hpp +++ b/src/account/account_repository.hpp @@ -43,6 +43,15 @@ class AccountRepository { CoinType coinType, const std::string &description ) = 0; + + virtual bool registerStoreTransaction( + const uint32_t &id, + CoinTransactionType type, + uint32_t amount, + CoinType coinType, + const std::string &description, + const time_t &time + ) = 0; }; constexpr auto g_accountRepository = AccountRepository::getInstance; diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index c3f02bfe972..50cec69d98d 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -164,6 +164,42 @@ bool AccountRepositoryDB::registerCoinsTransaction( return successful; }; +bool AccountRepositoryDB::registerStoreTransaction( + const uint32_t &id, + CoinTransactionType type, + uint32_t amount, + CoinType coinType, + const std::string &description, + const time_t &time +) { + + bool successful = g_database().executeQuery( + fmt::format( + "INSERT INTO `store_history` (`account_id`, `description`, `coin_amount`, `coin_type`, `type`, `created_at`) VALUES ({}, {}, {}, {}, {}, {})", + id, + g_database().escapeString(description), + amount, + coinType, + type, + time + ) + ); + + if (!successful) { + g_logger().error( + "Error registering coin transaction! account_id:[{}], type:[{}], coin_type:[{}], coins:[{}], description:[{}], time:[{}]", + id, + type, + coinType, + amount, + g_database().escapeString(description), + time + ); + } + + return successful; +}; + bool AccountRepositoryDB::loadAccountPlayers(std::unique_ptr &acc) const { auto result = g_database().storeQuery( fmt::format("SELECT `name`, `deletion` FROM `players` WHERE `account_id` = {} ORDER BY `name` ASC", acc->id) diff --git a/src/account/account_repository_db.hpp b/src/account/account_repository_db.hpp index e7a11c15e51..73fe79ab966 100644 --- a/src/account/account_repository_db.hpp +++ b/src/account/account_repository_db.hpp @@ -37,6 +37,15 @@ class AccountRepositoryDB final : public AccountRepository { const std::string &description ) override; + bool registerStoreTransaction( + const uint32_t &id, + CoinTransactionType type, + uint32_t amount, + CoinType coinType, + const std::string &description, + const time_t &time + ) override; + private: std::unordered_map coinTypeToColumn {}; diff --git a/src/canary_server.cpp b/src/canary_server.cpp index 8dfbcfc954c..491a00bbddd 100644 --- a/src/canary_server.cpp +++ b/src/canary_server.cpp @@ -23,6 +23,7 @@ #include "game/zones/zone.hpp" #include "io/io_bosstiary.hpp" #include "io/iomarket.hpp" +#include "io/io_store.hpp" #include "io/ioprey.hpp" #include "lib/thread/thread_pool.hpp" #include "lua/creature/events.hpp" @@ -353,6 +354,7 @@ void CanaryServer::loadModules() { modulesLoadHelper(Outfits::getInstance().loadFromXml(), "XML/outfits.xml"); modulesLoadHelper(Familiars::getInstance().loadFromXml(), "XML/familiars.xml"); modulesLoadHelper(g_imbuements().loadFromXml(), "XML/imbuements.xml"); + modulesLoadHelper(g_ioStore().loadFromXml(), "XML/store.xml"); modulesLoadHelper(g_storages().loadFromXML(), "XML/storages.xml"); modulesLoadHelper(Item::items.loadFromXml(), "items.xml"); diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 559045fdb9b..100b192dd2e 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -63,6 +63,7 @@ enum ConfigKey_t : uint16_t { EMOTE_SPELLS, ENABLE_PLAYER_PUT_ITEM_IN_AMMO_SLOT, ENABLE_SUPPORT_OUTFIT, + INSTANT_DAILY_REWARD_ACCESS_AMOUNT, EX_ACTIONS_DELAY_INTERVAL, EXP_FROM_PLAYERS_LEVEL_RANGE, EXPERIENCE_FROM_PLAYERS, @@ -192,6 +193,7 @@ enum ConfigKey_t : uint16_t { PREY_ENABLED, PREY_FREE_REROLL_TIME, PREY_FREE_THIRD_SLOT, + PREY_MAX_CARDS_AMOUNT, PREY_REROLL_PRICE_LEVEL, PREY_SELECTION_LIST_PRICE, PROTECTION_LEVEL, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1c00df76c2f..44553a44e66 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -258,6 +258,7 @@ bool ConfigManager::load() { loadIntConfig(L, HOUSE_BUY_LEVEL, "houseBuyLevel", 0); loadIntConfig(L, HOUSE_LOSE_AFTER_INACTIVITY, "houseLoseAfterInactivity", 0); loadIntConfig(L, HOUSE_PRICE_PER_SQM, "housePriceEachSQM", 1000); + loadIntConfig(L, INSTANT_DAILY_REWARD_ACCESS_AMOUNT, "instantDailyRewardAccessAmount", 90); loadIntConfig(L, KICK_AFTER_MINUTES, "kickIdlePlayerAfterMinutes", 15); loadIntConfig(L, LOOTPOUCH_MAXLIMIT, "lootPouchMaxLimit", 2000); loadIntConfig(L, LOW_LEVEL_BONUS_EXP, "lowLevelBonusExp", 50); @@ -289,6 +290,7 @@ bool ConfigManager::load() { loadIntConfig(L, PREY_BONUS_REROLL_PRICE, "preyBonusRerollPrice", 1); loadIntConfig(L, PREY_BONUS_TIME, "preyBonusTime", 7200); loadIntConfig(L, PREY_FREE_REROLL_TIME, "preyFreeRerollTime", 72000); + loadIntConfig(L, PREY_MAX_CARDS_AMOUNT, "preyMaxCardsAmount", 50); loadIntConfig(L, PREY_REROLL_PRICE_LEVEL, "preyRerollPricePerLevel", 200); loadIntConfig(L, PREY_SELECTION_LIST_PRICE, "preySelectListPrice", 5); loadIntConfig(L, PROTECTION_LEVEL, "protectionLevel", 1); diff --git a/src/creatures/CMakeLists.txt b/src/creatures/CMakeLists.txt index c87a45a0aa0..c09f411ab2d 100644 --- a/src/creatures/CMakeLists.txt +++ b/src/creatures/CMakeLists.txt @@ -25,6 +25,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE players/cyclopedia/player_badge.cpp players/cyclopedia/player_cyclopedia.cpp players/cyclopedia/player_title.cpp + players/gamestore/player_store_detail.cpp players/wheel/player_wheel.cpp players/wheel/wheel_gems.cpp players/vocations/vocation.cpp diff --git a/src/creatures/creatures_definitions.hpp b/src/creatures/creatures_definitions.hpp index dd554c086b3..a83ab38cae3 100644 --- a/src/creatures/creatures_definitions.hpp +++ b/src/creatures/creatures_definitions.hpp @@ -1513,11 +1513,12 @@ struct MarketOfferEx { }; struct HistoryMarketOffer { - uint32_t timestamp; - uint64_t price; - uint16_t itemId; - uint16_t amount; - uint8_t tier; + uint32_t createdAt {}; + uint32_t timestamp {}; + uint64_t price {}; + uint16_t itemId {}; + uint16_t amount {}; + uint8_t tier {}; MarketOfferState_t state; }; diff --git a/src/creatures/players/gamestore/player_store_detail.cpp b/src/creatures/players/gamestore/player_store_detail.cpp new file mode 100644 index 00000000000..db56ccff14e --- /dev/null +++ b/src/creatures/players/gamestore/player_store_detail.cpp @@ -0,0 +1,37 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#include "pch.hpp" + +#include "creatures/players/gamestore/player_store_detail.hpp" + +#include "kv/kv.hpp" + +std::string StoreDetail::toString() const { + return fmt::format("Description: {}, Coin Amount: {}, CreatedAt: {}, Is Gold: {}", description, coinAmount, createdAt, isGold); +} + +ValueWrapper StoreDetail::serialize() const { + return { + { "description", description }, + { "coinAmount", coinAmount }, + { "created_at", createdAt }, + { "is_gold", isGold } + }; +} + +StoreDetail StoreDetail::deserialize(const ValueWrapper &val) { + auto map = val.get(); + return { + map["description"]->get(), + map["coinAmount"]->get(), + map["created_at"]->get(), + map["is_gold"]->get() + }; +} diff --git a/src/creatures/players/gamestore/player_store_detail.hpp b/src/creatures/players/gamestore/player_store_detail.hpp new file mode 100644 index 00000000000..9518d98d49e --- /dev/null +++ b/src/creatures/players/gamestore/player_store_detail.hpp @@ -0,0 +1,30 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#include "kv/value_wrapper_proto.hpp" + +class ValueWrapper; + +#ifndef USE_PRECOMPILED_HEADERS + #include + #include +#endif + +struct StoreDetail { + std::string description; + int32_t coinAmount {}; + int createdAt {}; + bool isGold {}; + + std::string toString() const; + ValueWrapper serialize() const; + static StoreDetail deserialize(const ValueWrapper &val); +}; diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 88223534497..edf9a71fe8b 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -25,6 +25,7 @@ #include "creatures/players/cyclopedia/player_badge.hpp" #include "creatures/players/cyclopedia/player_cyclopedia.hpp" #include "creatures/players/cyclopedia/player_title.hpp" +#include "creatures/players/gamestore/player_store_detail.hpp" #include "creatures/players/grouping/party.hpp" #include "creatures/players/imbuements/imbuements.hpp" #include "creatures/players/storages/storages.hpp" @@ -46,7 +47,9 @@ #include "grouping/guild.hpp" #include "io/iobestiary.hpp" #include "io/iologindata.hpp" +#include "io/functions/iologindata_save_player.hpp" #include "io/ioprey.hpp" +#include "io/io_store.hpp" #include "items/bed.hpp" #include "items/containers/depot/depotchest.hpp" #include "items/containers/depot/depotlocker.hpp" @@ -2258,9 +2261,9 @@ void Player::sendExperienceTracker(int64_t rawExp, int64_t finalExp) const { } } -void Player::sendOutfitWindow() const { +void Player::sendOutfitWindow(uint16_t tryOutfit /* = 0*/, uint16_t tryMount /* = 0*/) const { if (client) { - client->sendOutfitWindow(); + client->sendOutfitWindow(tryOutfit, tryMount); } } @@ -9584,8 +9587,243 @@ void Player::openPlayerContainers() { } } -// Quickloot +// Store functions +void Player::openStore() { + if (client) { + client->openStore(); + } +} + +void Player::sendStoreHistory(uint32_t page) const { + if (client) { + client->sendStoreHistory(page); + } +} + +void Player::sendStoreSuccess(const std::string &successMessage) { + if (client) { + client->sendStoreSuccess(successMessage); + } +} + +void Player::sendStoreError(StoreErrors_t errorType, const std::string &errorMessage) { + if (client) { + client->sendStoreError(errorType, errorMessage); + } +} + +std::vector &Player::getStoreHistory() { + return storeHistoryVector; +} + +void Player::setStoreHistory(const StoreHistory &history) { + storeHistoryVector.push_back(history); +} + +void Player::addStoreHistory(bool fromMarket, const std::string &playerName, time_t createdAt, uint32_t coinAmount, StoreDetailType type, MarketAction_t action, const std::string &description, uint64_t totalPrice /* = 0*/) { + StoreHistory storeHistory; + storeHistory.fromMarket = fromMarket; + storeHistory.createdAt = createdAt; + storeHistory.coinAmount = action == MARKETACTION_SELL ? -static_cast(coinAmount) : static_cast(coinAmount); + storeHistory.type = type; + storeHistory.description = description; + storeHistory.playerName = playerName; + storeHistory.totalPrice = action == MARKETACTION_SELL ? static_cast(totalPrice) : -static_cast(totalPrice); + + if (!IOLoginDataSave::savePlayerStoreHistory(getPlayer())) { + g_logger().error("[{}] Failed to save store history for player {}", __FUNCTION__, getName()); + } + + setStoreHistory(storeHistory); +} + +void Player::addStoreDetail(const std::string &description, int32_t coinAmount, int createdAt, bool isGold /* = false*/) const { + auto detailCreatedAt = getTimeNow(); + StoreDetail detail; + detail.description = description; + detail.coinAmount = coinAmount; + detail.createdAt = detailCreatedAt; + detail.isGold = isGold; + + // Store the serialized StoreDetail in the KV + getStoreHistoryScope(createdAt)->set(std::to_string(detailCreatedAt), detail.serialize()); + + g_logger().debug("Player: {}, {}", getName(), detail.toString()); +} + +std::vector> Player::getStoreHistoryDetails(int32_t createdAt) const { + // Vector to hold StoreDetail objects along with their keys + std::vector> details; + + // Get the scoped KV for the specific store history + auto historyScoped = getStoreHistoryScope(createdAt); + // Retrieve all details and store them in the container + for (const auto &createdAtKey : historyScoped->keys()) { + auto valueWrapper = historyScoped->get(createdAtKey); + if (!valueWrapper) { + g_logger().error("Failed to retrieve StoreDetail for ActionTimestamp={}", createdAtKey); + continue; + } + + // Deserialize the ValueWrapper into StoreDetail + StoreDetail detail = StoreDetail::deserialize(valueWrapper.value()); + + // Store the detail along with its creation date as a pair + details.emplace_back(createdAtKey, detail); + } + + // Sort the details by the createdAt field in ascending order + std::sort(details.begin(), details.end(), [](const auto &lhs, const auto &rhs) { + return lhs.second.createdAt < rhs.second.createdAt; + }); + + return details; +} + +std::shared_ptr Player::getStoreHistoryScope(int32_t createdAt) const { + // Access the complete KV scope for store history + return getStoreDetailScope(createdAt)->scoped("history"); +} + +std::shared_ptr Player::getStoreDetailScope(int32_t createdAt) const { + // Access the complete KV scope for store detail + auto storeDetailScope = kv()->scoped("store-detail"); + auto createdAtScope = storeDetailScope->scoped(std::to_string(createdAt)); + return createdAtScope; +} + +bool Player::canBuyStoreOffer(const Offer* offer) { + auto canBuy = true; + auto offerType = offer->getType(); + switch (offerType) { + case OfferTypes_t::OUTFIT: { + auto offerOutfitId = offer->getOutfitIds(); + auto playerLookType = (getSex() == PLAYERSEX_FEMALE ? offerOutfitId.femaleId : offerOutfitId.maleId); + uint8_t addons = playerLookType >= 962 && playerLookType <= 975 ? 0 : 3; + + if (canWear(playerLookType, addons)) { + canBuy = false; + } + break; + } + + case OfferTypes_t::MOUNT: { + auto mount = g_game().mounts->getMountByID(offer->getID()); + + if (hasMount(mount)) { + canBuy = false; + } + break; + } + + case OfferTypes_t::EXPBOOST: { + auto expBoostCount = getStorageValue(STORAGEVALUE_EXPBOOST); + + if (expBoostCount >= 6 || getXpBoostTime() > 0) { + canBuy = false; + } + break; + } + case OfferTypes_t::PREYSLOT: { + const auto &thirdSlot = getPreySlotById(PreySlot_Three); + + if (thirdSlot->state != PreyDataState_Locked) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::PREYBONUS: { + auto cardsAmount = offer->getCount(); + if (getPreyCards() + cardsAmount >= g_configManager().getNumber(PREY_MAX_CARDS_AMOUNT)) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::BLESSINGS: { + auto blessId = offer->getID(); + if (!magic_enum::enum_contains(blessId)) { + sendStoreError(StoreErrors_t::PURCHASE, "An error has occurred, please contact your administrator."); + g_logger().error("[{}] invalid blessing id: {}, for player: {}", __FUNCTION__, blessId, getName()); + break; + } + + auto blessingAmount = getBlessingCount(blessId); + if (blessingAmount >= STORE_BLESSING_MAX_AMOUNT) { + canBuy = false; + } + break; + } + + case OfferTypes_t::ALLBLESSINGS: { + for (auto bless : magic_enum::enum_values()) { + auto blessingAmount = getBlessingCount(enumToValue(bless)); + if (blessingAmount >= STORE_BLESSING_MAX_AMOUNT) { + canBuy = false; + break; + } + } + break; + } + + case OfferTypes_t::POUCH: { + auto pouchStorageValue = getStorageValue(STORAGEVALUE_POUCH); + + if (pouchStorageValue == 1) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::INSTANT_REWARD_ACCESS: { + auto offerInstantAmount = offer->getCount(); + auto playerInstantAmount = getStorageValue(STORAGEVALUE_REWARD_ACCESS); + + if (playerInstantAmount + offerInstantAmount >= g_configManager().getNumber(INSTANT_DAILY_REWARD_ACCESS_AMOUNT)) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::CHARM_EXPANSION: { + if (hasCharmExpansion()) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::HUNTINGSLOT: { + const auto &thirdSlot = getTaskHuntingSlotById(PreySlot_Three); + if (thirdSlot->state != PreyTaskDataState_Locked) { + canBuy = false; + } + + break; + } + + case OfferTypes_t::TEMPLE: { + if (!canLogout()) { + canBuy = false; + } + + break; + } + + default: + break; + } + + return canBuy; +} + +// Quickloot void Player::sendLootContainers() const { if (client) { client->sendLootContainers(); diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 6ab3be4ca7a..9a34473c355 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -48,6 +48,7 @@ class Container; class KV; class BedItem; class Npc; +class Offer; struct ModalWindow; struct Achievement; @@ -61,9 +62,13 @@ struct Group; struct Outfit_t; struct TextMessage; struct HighscoreCharacter; +struct StoreDetail; +struct StoreHistory; enum class PlayerIcon : uint8_t; enum class IconBakragore : uint8_t; +enum class StoreErrors_t : uint8_t; +enum class StoreDetailType : uint8_t; enum ObjectCategory_t : uint8_t; enum PreySlot_t : uint8_t; enum SpeakClasses : uint8_t; @@ -167,6 +172,12 @@ class Player final : public Creature, public Cylinder, public Bankable { void setName(const std::string &name) { this->name = name; } + const std::string &getNewName() const { + return m_newName; + } + void setNewName(const std::string &newName) { + this->m_newName = newName; + } const std::string &getTypeName() const override { return name; } @@ -879,7 +890,7 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendChannelsDialog() const; void sendOpenPrivateChannel(const std::string &receiver) const; void sendExperienceTracker(int64_t rawExp, int64_t finalExp) const; - void sendOutfitWindow() const; + void sendOutfitWindow(uint16_t tryOutfit = 0, uint16_t tryMount = 0) const; // Imbuements void onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm); void onClearImbuement(const std::shared_ptr &item, uint8_t slot); @@ -1264,6 +1275,20 @@ class Player final : public Creature, public Cylinder, public Bankable { uint16_t getPlayerVocationEnum() const; + // Store functions + void openStore(); + void sendStoreHistory(uint32_t page) const; + void sendStoreSuccess(const std::string &successMessage); + void sendStoreError(StoreErrors_t errorType, const std::string &errorMessage); + std::vector &getStoreHistory(); + void setStoreHistory(const StoreHistory &history); + void addStoreHistory(bool fromMarket, const std::string &playerName, time_t createdAt, uint32_t coinAmount, StoreDetailType type, MarketAction_t action, const std::string &description, uint64_t totalPrice = 0); + void addStoreDetail(const std::string &description, int32_t coinAmount, int createdAt, bool isGold = false) const; + std::vector> getStoreHistoryDetails(int32_t createdAt) const; + std::shared_ptr getStoreHistoryScope(int32_t createdAt) const; + std::shared_ptr getStoreDetailScope(int32_t createdAt) const; + bool canBuyStoreOffer(const Offer* offer); + private: friend class PlayerLock; std::mutex mutex; @@ -1350,6 +1375,7 @@ class Player final : public Creature, public Cylinder, public Bankable { std::map, std::shared_ptr>> m_managedContainers; std::vector forgeHistoryVector; + std::vector storeHistoryVector; std::vector quickLootListItemIds; @@ -1371,6 +1397,7 @@ class Player final : public Creature, public Cylinder, public Bankable { std::unordered_set> m_bosstiaryMonsterTracker; std::string name; + std::string m_newName; std::string guildNick; std::string loyaltyTitle; diff --git a/src/database/database.hpp b/src/database/database.hpp index 69c47d324ad..7f946038b0d 100644 --- a/src/database/database.hpp +++ b/src/database/database.hpp @@ -88,7 +88,7 @@ class DBResult { auto it = listNames.find(s); if (it == listNames.end()) { g_logger().error("[DBResult::getNumber] - Column '{}' doesn't exist in the result set", s); - return T(); + return {}; } if (row[it->second] == nullptr) { @@ -154,7 +154,6 @@ class DBResult { g_logger().error("Column '{}' has a value out of range, error code: {}", s, e.what()); data = T(); } - return data; } diff --git a/src/enums/account_coins.hpp b/src/enums/account_coins.hpp index 0980500aaaa..26cf65116be 100644 --- a/src/enums/account_coins.hpp +++ b/src/enums/account_coins.hpp @@ -15,7 +15,7 @@ enum class CoinTransactionType : uint8_t { }; enum class CoinType : uint8_t { - Normal = 1, - Tournament = 2, - Transferable = 3 + Normal = 0, + Transferable = 1, + Tournament = 2 }; diff --git a/src/enums/player_store_enums.hpp b/src/enums/player_store_enums.hpp new file mode 100644 index 00000000000..e798abe1be7 --- /dev/null +++ b/src/enums/player_store_enums.hpp @@ -0,0 +1,112 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2024 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.com/ + */ + +#pragma once + +#ifndef USE_PRECOMPILED_HEADERS + #include +#endif + +// XML +enum class OfferTypes_t : uint8_t { + NONE = 0, + ITEM = 1, + STACKABLE = 2, + CHARGES = 3, + OUTFIT = 4, + MOUNT = 5, + NAMECHANGE = 6, + SEXCHANGE = 7, + HOUSE = 8, + EXPBOOST = 9, + PREYSLOT = 10, + PREYBONUS = 11, + TEMPLE = 12, + BLESSINGS = 13, + ALLBLESSINGS = 14, + PREMIUM = 15, + POUCH = 16, + INSTANT_REWARD_ACCESS = 17, + CHARM_EXPANSION = 18, + HUNTINGSLOT = 19, + HIRELING = 20, + HIRELING_NAMECHANGE = 21, + HIRELING_SEXCHANGE = 22, + HIRELING_SKILL = 23, + HIRELING_OUTFIT = 24, + LOOKTYPE = 25 +}; + +enum class States_t : uint8_t { + NONE = 0, + NEW = 1, + SALE = 2, + TIMED = 3 +}; + +// Internal +enum class ConverType_t : uint8_t { + NONE = 0, + MOUNT = 1, + LOOKTYPE = 2, + ITEM = 3, + OUTFIT = 4 +}; + +enum class SubActions_t : uint8_t { + PREY_THIRDSLOT_REAL = 0, + PREY_WILDCARD = 1, + INSTANT_REWARD = 2, + BLESSING_TWIST = 3, + BLESSING_SOLITUDE = 4, + BLESSING_PHOENIX = 5, + BLESSING_SUNS = 6, + BLESSING_SPIRITUAL = 7, + BLESSING_EMBRACE = 8, + BLESSING_HEART = 9, + BLESSING_BLOOD = 10, + BLESSING_ALL_PVE = 11, + BLESSING_ALL_PVP = 12, + CHARM_EXPANSION = 13, + TASKHUNTING_THIRDSLOT = 14, + PREY_THIRDSLOT_REDIRECT = 15 +}; + +enum class HistoryTypes_t : uint8_t { + NONE = 0, + GIFT = 1, + REFUND = 2 +}; + +enum class StoreDetailType : uint8_t { + Finished = 0, + Created = 1, +}; + +enum class StoreErrors_t : uint8_t { + PURCHASE = 0, + NETWORK = 1, + HISTORY = 2, + TRANSFER = 3, + INFORMATION = 4 +}; + +enum class BannerType : uint8_t { + COLLECTION = 2, + OFFER = 4 +}; + +// Others +enum class ActionType_t : uint8_t { + OPEN_HOME = 0, + OPEN_PREMIUM_BOOST = 1, + OPEN_CATEGORY = 2, + OPEN_USEFUL_THINGS = 3, + OPEN_OFFER = 4, +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index 5c7dc7aaf16..27fc942eef0 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -39,6 +39,7 @@ #include "io/ioguild.hpp" #include "io/iologindata.hpp" #include "io/iomarket.hpp" +#include "io/io_store.hpp" #include "io/ioprey.hpp" #include "items/bed.hpp" #include "items/containers/inbox/inbox.hpp" @@ -69,6 +70,7 @@ #include "enums/account_group_type.hpp" #include "enums/account_type.hpp" #include "enums/object_category.hpp" +#include "enums/player_blessings.hpp" #include @@ -1922,7 +1924,7 @@ ReturnValue Game::checkMoveItemToCylinder(const std::shared_ptr &player, return RETURNVALUE_ITEMCANNOTBEMOVEDPOUCH; } - // prevent move up from ponch to store inbox. + // prevent move up from pouch to store inbox if (!item->canBeMovedToStore() && fromCylinder->getContainer() && fromCylinder->getContainer()->getID() == ITEM_GOLD_POUCH) { return RETURNVALUE_NOTBOUGHTINSTORE; } @@ -1990,7 +1992,7 @@ ReturnValue Game::checkMoveItemToCylinder(const std::shared_ptr &player, } } - if (item->isStoreItem() && !house) { + if (item->isStoreItem() && (!item->isWrapable() || !house)) { return RETURNVALUE_ITEMCANNOTBEMOVEDTHERE; } } @@ -6060,7 +6062,7 @@ void Game::playerTurn(uint32_t playerId, Direction dir) { internalCreatureTurn(player, dir); } -void Game::playerRequestOutfit(uint32_t playerId) { +void Game::playerRequestOutfit(uint32_t playerId, uint16_t tryOutfit /* = 0*/, uint16_t tryMount /* = 0*/) { if (!g_configManager().getBoolean(ALLOW_CHANGEOUTFIT)) { return; } @@ -6070,7 +6072,7 @@ void Game::playerRequestOutfit(uint32_t playerId) { return; } - player->sendOutfitWindow(); + player->sendOutfitWindow(tryOutfit, tryMount); } void Game::playerToggleMount(uint32_t playerId, bool mount) { @@ -8993,7 +8995,9 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite // Make sure everything is ok before the create market offer starts if (!checkCanInitCreateMarketOffer(player, type, it, amount, price, offerStatus)) { - g_logger().error("{} - Player {} had an error on init offer on the market, error code: {}", __FUNCTION__, player->getName(), offerStatus.str()); + if (!offerStatus.str().empty()) { + g_logger().error("{} - Player {} had an error on init offer on the market, error code: {}", __FUNCTION__, player->getName(), offerStatus.str()); + } return; } @@ -9001,6 +9005,9 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite uint64_t minFee = std::min(100000, calcFee); uint64_t fee = std::max(20, minFee); + uint64_t totalPrice = price * amount; + // Store the timestamp to ensure consistency across multiple calls, avoiding slight differences in time + auto createdAt = getTimeNow(); if (type == MARKETACTION_SELL) { if (fee > (player->getBankBalance() + player->getMoney())) { offerStatus << "Fee is greater than player money"; @@ -9023,6 +9030,12 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite // Do not register a transaction for coins creating an offer player->getAccount()->removeCoins(CoinType::Transferable, static_cast(amount), ""); + + player->addStoreHistory(true, player->getName(), createdAt, amount, StoreDetailType::Created, MARKETACTION_SELL, "Sell Offer Placed in The Market", totalPrice); + auto description = "Sell Offer Placed in the Market"; + player->addStoreDetail(description, -amount, createdAt); + + g_logger().info("[{}] Player {} created a sell offer for {} coins", __FUNCTION__, player->getName(), amount); } else { if (!removeOfferItems(player, depotLocker, it, amount, tier, offerStatus)) { g_logger().error("[{}] failed to remove item with id {}, from player {}, errorcode: {}", __FUNCTION__, it.id, player->getName(), offerStatus.str()); @@ -9033,7 +9046,6 @@ void Game::playerCreateMarketOffer(uint32_t playerId, uint8_t type, uint16_t ite g_game().removeMoney(player, fee, 0, true); g_metrics().addCounter("balance_decrease", fee, { { "player", player->getName() }, { "context", "market_fee" } }); } else { - uint64_t totalPrice = price * amount; totalPrice += fee; if (totalPrice > (player->getMoney() + player->getBankBalance())) { offerStatus << "Fee is greater than player money (buy offer)"; @@ -9090,10 +9102,11 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 return; } + uint64_t totalPrice = offer.price * offer.amount; const auto &playerInbox = player->getInbox(); if (offer.type == MARKETACTION_BUY) { - player->setBankBalance(player->getBankBalance() + offer.price * offer.amount); - g_metrics().addCounter("balance_decrease", offer.price * offer.amount, { { "player", player->getName() }, { "context", "market_purchase" } }); + player->setBankBalance(player->getBankBalance() + totalPrice); + g_metrics().addCounter("balance_decrease", totalPrice, { { "player", player->getName() }, { "context", "market_purchase" } }); // Send market window again for update stats player->sendMarketEnter(player->getLastDepotId()); } else { @@ -9105,6 +9118,9 @@ void Game::playerCancelMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { // Do not register a transaction for coins upon cancellation player->getAccount()->addCoins(CoinType::Transferable, offer.amount, ""); + + auto description = "Sell Offer Cancelled or Expired"; + player->addStoreDetail(description, offer.amount, offer.timestamp); } else if (it.stackable) { uint16_t tmpAmount = offer.amount; @@ -9193,6 +9209,8 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 uint64_t totalPrice = offer.price * amount; + // Store the timestamp to ensure consistency across multiple calls, avoiding slight differences in time + auto createdAt = getTimeNow(); // The player has an offer to by something and someone is going to sell to item type // so the market action is 'buy' as who created the offer is buying. if (offer.type == MARKETACTION_BUY) { @@ -9240,6 +9258,8 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 amount, "Sold on Market" ); + + player->addStoreHistory(true, player->getName(), createdAt, amount, StoreDetailType::Finished, MARKETACTION_SELL, "Transferred via the Market", totalPrice); } else { if (!removeOfferItems(player, depotLocker, it, amount, offer.tier, offerStatus)) { g_logger().error("[{}] failed to remove item with id {}, from player {}, errorcode: {}", __FUNCTION__, it.id, player->getName(), offerStatus.str()); @@ -9265,6 +9285,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { buyerPlayer->getAccount()->addCoins(CoinType::Transferable, amount, "Purchased on Market"); + buyerPlayer->addStoreHistory(true, buyerPlayer->getName(), createdAt, amount, StoreDetailType::Finished, MARKETACTION_BUY, "Purchased via the Market", totalPrice); } else if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { @@ -9314,7 +9335,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 return; } - if (player == sellerPlayer || player->getAccount() == sellerPlayer->getAccount()) { + if (player == sellerPlayer || player->getAccountId() == sellerPlayer->getAccountId()) { player->sendTextMessage(MESSAGE_MARKET, "You cannot accept your own offer."); return; } @@ -9336,6 +9357,7 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 if (it.id == ITEM_STORE_COIN) { player->getAccount()->addCoins(CoinType::Transferable, amount, "Purchased on Market"); + player->addStoreHistory(true, player->getName(), createdAt, amount, StoreDetailType::Finished, MARKETACTION_BUY, "Purchased via the Market", totalPrice); } else if (it.stackable) { uint16_t tmpAmount = amount; while (tmpAmount > 0) { @@ -9390,6 +9412,23 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 g_metrics().addCounter("balance_increase", totalPrice, { { "player", sellerPlayer->getName() }, { "context", "market_sale" } }); if (it.id == ITEM_STORE_COIN) { sellerPlayer->getAccount()->registerCoinTransaction(CoinTransactionType::Remove, CoinType::Transferable, amount, "Sold on Market"); + sellerPlayer->addStoreHistory(true, sellerPlayer->getName(), createdAt, amount, StoreDetailType::Finished, MARKETACTION_SELL, "Transferred via the Market", totalPrice); + // Add store detail for seller + auto description = fmt::format("Sold {} Tibia Coins", amount); + sellerPlayer->addStoreDetail(description, totalPrice, offer.timestamp, true); + + // Check and update the sold coin amount for the seller player + auto storeHistoryScope = sellerPlayer->getStoreDetailScope(offer.timestamp); + auto soldCoinAmountOpt = storeHistoryScope->get("sold-coin-amount"); + auto soldCoinAmount = soldCoinAmountOpt ? soldCoinAmountOpt->getNumber() : 0; + storeHistoryScope->set("sold-coin-amount", soldCoinAmount + amount); + + // Check and update the received gold amount for the seller player + auto receivedGoldAmountOpt = storeHistoryScope->get("received-gold-amount"); + auto receivedGoldAmount = receivedGoldAmountOpt ? receivedGoldAmountOpt->getNumber() : 0; + storeHistoryScope->set("received-gold-amount", receivedGoldAmount + totalPrice); + + g_logger().info("Offer timestamp: {}, sold coin amount: {}, received gold amount: {}", offer.timestamp, soldCoinAmount + amount, receivedGoldAmount + totalPrice); } if (it.id != ITEM_STORE_COIN) { @@ -9412,9 +9451,9 @@ void Game::playerAcceptMarketOffer(uint32_t playerId, uint32_t timestamp, uint16 const int32_t marketOfferDuration = g_configManager().getNumber(MARKET_OFFER_DURATION); - IOMarket::appendHistory(player->getGUID(), (offer.type == MARKETACTION_BUY ? MARKETACTION_SELL : MARKETACTION_BUY), offer.itemId, amount, offer.price, time(nullptr), offer.tier, OFFERSTATE_ACCEPTEDEX); + IOMarket::appendHistory(player->getGUID(), (offer.type == MARKETACTION_BUY ? MARKETACTION_SELL : MARKETACTION_BUY), offer.itemId, amount, offer.price, createdAt, offer.tier, OFFERSTATE_ACCEPTEDEX); - IOMarket::appendHistory(offer.playerId, offer.type, offer.itemId, amount, offer.price, time(nullptr), offer.tier, OFFERSTATE_ACCEPTED); + IOMarket::appendHistory(offer.playerId, offer.type, offer.itemId, amount, offer.price, createdAt, offer.tier, OFFERSTATE_ACCEPTED); offer.amount -= amount; @@ -10408,35 +10447,131 @@ bool Game::addInfluencedMonster(const std::shared_ptr &monster) { return false; } -bool Game::addItemStoreInbox(const std::shared_ptr &player, uint32_t itemId) { +bool Game::processHouseOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t charges /* = 0*/) { + const auto &storeInbox = player->getStoreInbox(); + if (!storeInbox) { + return false; + } + const auto &decoKit = Item::CreateItem(ITEM_DECORATION_KIT, 1); if (!decoKit) { return false; } + const ItemType &itemType = Item::items[itemId]; - std::string description = fmt::format("Unwrap it in your own house to create a <{}>.", itemType.name); + std::string description = fmt::format("You bought this item in the Store.\nUnwrap it in your own house to create a <{}>.", itemType.name); decoKit->setAttribute(ItemAttribute_t::DESCRIPTION, description); decoKit->setCustomAttribute("unWrapId", static_cast(itemId)); - const auto &thing = player->getThing(CONST_SLOT_STORE_INBOX); - if (!thing) { + if (charges > 0) { + decoKit->setAttribute(ItemAttribute_t::CHARGES, charges); + decoKit->setAttribute(ItemAttribute_t::DATE, charges); + } + + decoKit->setAttribute(ItemAttribute_t::STORE, getTimeNow()); + + if (internalAddItem(storeInbox, decoKit) != RETURNVALUE_NOERROR) { + return false; + } + + return true; +} + +bool Game::processChargesOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t charges /* = 0*/, bool movable /* = false*/) { + const auto &storeInbox = player->getStoreInbox(); + if (!storeInbox) { + return false; + } + + const auto &newItem = Item::CreateItem(itemId, 1); + if (!newItem) { + return false; + } + + if (charges > 0) { + newItem->setAttribute(ItemAttribute_t::CHARGES, charges); + } + + if (!movable) { + newItem->setAttribute(ItemAttribute_t::STORE, getTimeNow()); + } + + newItem->setOwner(player); + + auto ret = internalAddItem(storeInbox, newItem); + if (ret != RETURNVALUE_NOERROR) { + return false; + } + + return true; +} + +bool Game::processStackableOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t amount /* = 1*/, bool movable /* = false*/) { + const auto &storeInbox = player->getStoreInbox(); + if (!storeInbox) { + return false; + } + + int32_t remainingAmount = amount; + do { + const auto amountToAdd = (remainingAmount >= 100) ? 100 : remainingAmount; + const auto &newItem = Item::CreateItem(itemId, amountToAdd); + if (!newItem) { + return false; + } + + if (!movable) { + newItem->setAttribute(ItemAttribute_t::STORE, getTimeNow()); + } + + newItem->setOwner(player); + + const auto ret = internalAddItem(storeInbox, newItem); + if (ret != RETURNVALUE_NOERROR) { + return false; + } + remainingAmount -= amountToAdd; + } while (remainingAmount > 0); + + return true; +} + +bool Game::processNameChangeOffer(const std::shared_ptr &player, std::string name) { + trimString(name); + + auto isValidName = validateName(name); + if (isValidName != VALID) { return false; } - const auto &inboxItem = thing->getItem(); - if (!inboxItem) { + capitalizeWords(name); + + if (g_monsters().getMonsterType(name, true)) { + return false; + } else if (getNpcByName(name)) { return false; } - const auto &inboxContainer = inboxItem->getContainer(); - if (!inboxContainer) { + DBResult_ptr result = g_database().storeQuery(fmt::format("SELECT `id` FROM `players` WHERE `name` = {}", g_database().escapeString(name))); + if (result) { return false; } - if (internalAddItem(inboxContainer, decoKit) != RETURNVALUE_NOERROR) { - inboxContainer->internalAddThing(decoKit); + player->setNewName(name); + + return true; +} + +bool Game::processTempleOffer(const std::shared_ptr &player) { + if (!player->canLogout()) { + return false; } + const auto &position = player->getTemplePosition(); + playerTeleport(player->getID(), position); + addMagicEffect(position, CONST_ME_TELEPORT); + player->sendTextMessage(MESSAGE_EVENT_ADVANCE, "You have been teleported to your hometown."); + return true; } @@ -10559,6 +10694,385 @@ void Game::playerRewardChestCollect(uint32_t playerId, const Position &pos, uint } } +void Game::playerOpenStore(uint32_t playerId) { + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (player->isUIExhausted()) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return; + } + + player->updateUIExhausted(); + player->openStore(); +} + +void Game::playerCoinTransfer(uint32_t playerId, const std::string &receptorName, uint32_t coinAmount) { + const auto &playerDonator = getPlayerByID(playerId); + if (!playerDonator) { + return; + } + + if (playerDonator->isUIExhausted(1000)) { + playerDonator->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return; + } + + const auto &playerReceptor = getPlayerByName(receptorName, true); + if (!playerReceptor) { + return; + } + + if (playerDonator == playerReceptor || playerDonator->getAccountId() == playerReceptor->getAccountId()) { + playerDonator->sendStoreError(StoreErrors_t::TRANSFER, "You cannot gift Tibia Coins to characters of your own account."); + return; + } + + auto [transferableCoins, result] = playerDonator->getAccount()->getCoins(CoinType::Transferable); + if (coinAmount > transferableCoins) { + playerDonator->sendStoreError(StoreErrors_t::TRANSFER, "You don't have enough coins."); + return; + } + + auto createdAt = getTimeNow(); + std::string historyDesc = fmt::format("{} gifted to {}", playerDonator->getName(), playerReceptor->getName()); + playerDonator->getAccount()->removeCoins(CoinType::Transferable, coinAmount, historyDesc); + playerReceptor->getAccount()->addCoins(CoinType::Transferable, coinAmount, historyDesc); + + playerDonator->addStoreHistory(false, playerDonator->getName(), createdAt, coinAmount, StoreDetailType::Finished, MARKETACTION_SELL, historyDesc); + playerReceptor->addStoreHistory(false, playerReceptor->getName(), createdAt, coinAmount, StoreDetailType::Finished, MARKETACTION_BUY, historyDesc); + playerReceptor->sendCoinBalance(); + playerDonator->openStore(); + playerDonator->updateUIExhausted(); +} + +void Game::playerOpenStoreHistory(uint32_t playerId, uint32_t page) { + const auto &player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (player->isUIExhausted()) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return; + } + + player->updateUIExhausted(); + player->sendStoreHistory(page); +} + +void Game::playerBuyStoreOffer(uint32_t playerId, const Offer* offer, std::string newName, uint8_t sexId) { + const auto &player = getPlayerByID(playerId); + if (!player) { + return; + } + + if (!offer) { + return; + } + + if (player->isUIExhausted()) { + player->sendCancelMessage(RETURNVALUE_YOUAREEXHAUSTED); + return; + } + + std::string errorMessage = "An error has occurred, please contact your administrator."; + bool success = false; + auto offerType = offer->getType(); + switch (offerType) { + case OfferTypes_t::HOUSE: { + auto itemId = offer->getItemId(); + auto offerAmount = offer->getCount(); + + success = processHouseOffer(player, itemId, offerAmount); + break; + } + + case OfferTypes_t::CHARGES: { + auto itemId = offer->getItemId(); + auto itemCharges = offer->getCount(); + auto isMovable = offer->isMovable(); + + success = processChargesOffer(player, itemId, itemCharges, isMovable); + break; + } + + case OfferTypes_t::ITEM: + case OfferTypes_t::STACKABLE: { + auto itemId = offer->getItemId(); + auto itemAmount = offer->getCount(); + auto isMovable = offer->isMovable(); + + success = processStackableOffer(player, itemId, itemAmount, isMovable); + break; + } + + case OfferTypes_t::POUCH: { + auto itemId = offer->getItemId(); + auto pouchStorageValue = player->getStorageValue(STORAGEVALUE_POUCH); + + if (pouchStorageValue == 1) { + break; + } + + player->addStorageValue(STORAGEVALUE_POUCH, 1); + + success = processStackableOffer(player, itemId, false); + break; + } + + case OfferTypes_t::OUTFIT: { + auto offerOutfitId = offer->getOutfitIds(); + auto playerLookType = (player->getSex() == PLAYERSEX_FEMALE ? offerOutfitId.femaleId : offerOutfitId.maleId); + auto addons = playerLookType >= 962 && playerLookType <= 975 ? 0 : 3; + + if (player->canWear(playerLookType, addons)) { + errorMessage = "You already own this outfit."; + break; + } + + player->addOutfit(offerOutfitId.maleId, addons); + player->addOutfit(offerOutfitId.femaleId, addons); + success = true; + break; + } + + case OfferTypes_t::MOUNT: { + auto mount = g_game().mounts->getMountByID(offer->getID()); + if (!mount) { + break; + } + + if (player->hasMount(mount)) { + errorMessage = "You already own this mount."; + break; + } + + if (!player->tameMount(mount->id)) { + break; + } + + success = true; + break; + } + + case OfferTypes_t::NAMECHANGE: { + success = processNameChangeOffer(player, newName); + if (!success) { + errorMessage = "This name is not available."; + } + break; + } + + case OfferTypes_t::SEXCHANGE: { + Outfit_t outfit = player->getCurrentOutfit(); + if (player->getSex() == PLAYERSEX_FEMALE) { + player->setSex(PLAYERSEX_MALE); + outfit.lookType = 128; + } else { + player->setSex(PLAYERSEX_FEMALE); + outfit.lookType = 136; + } + + outfit.lookAddons = 0; + playerChangeOutfit(playerId, outfit); + success = true; + break; + } + + case OfferTypes_t::EXPBOOST: { + auto currentExpBoost = player->getXpBoostTime(); + auto expBoostCount = player->getStorageValue(STORAGEVALUE_EXPBOOST); + + player->setXpBoostPercent(50); + player->setXpBoostTime(currentExpBoost + 3600); + + if (expBoostCount == -1 || expBoostCount == 6) { + expBoostCount = 1; + } + + player->addStorageValue(STORAGEVALUE_EXPBOOST, expBoostCount + 1); + player->sendStats(); + success = true; + break; + } + + case OfferTypes_t::TEMPLE: { + success = processTempleOffer(player); + if (!success) { + errorMessage = "You cannot make this purchase as long as your characters has a logout block."; + } + + break; + } + + case OfferTypes_t::BLESSINGS: { + auto blessId = offer->getID(); + if (!magic_enum::enum_contains(blessId)) { + g_logger().error("[{}] invalid blessing id: {}, for player: {}", __FUNCTION__, blessId, player->getName()); + break; + } + + player->addBlessing(blessId, offer->getCount()); + player->sendBlessStatus(); + + success = true; + break; + } + + case OfferTypes_t::ALLBLESSINGS: { + for (auto bless : magic_enum::enum_values()) { + player->addBlessing(enumToValue(bless), 1); + } + + player->sendBlessStatus(); + + success = true; + break; + } + + case OfferTypes_t::PREMIUM: { + auto premiumDaysLeft = player->getPremiumDays(); + if (premiumDaysLeft > 65175) { + break; + } + + int32_t premiumDays = static_cast(offer->getID()) - 3000; + player->getAccount()->addPremiumDays(premiumDays); + if (player->getAccount()->save() != AccountErrors_t::Ok) { + break; + } + + success = true; + break; + } + + case OfferTypes_t::PREYSLOT: { + const auto &thirdSlot = player->getPreySlotById(PreySlot_Three); + + if (thirdSlot->state != PreyDataState_Locked) { + break; + } + + thirdSlot->eraseBonus(); + thirdSlot->state = PreyDataState_Selection; + thirdSlot->reloadMonsterGrid(player->getPreyBlackList(), player->getLevel()); + player->reloadPreySlot(PreySlot_Three); + + success = true; + break; + } + + case OfferTypes_t::PREYBONUS: { + auto cardsAmount = offer->getCount(); + if (player->getPreyCards() + cardsAmount >= g_configManager().getNumber(PREY_MAX_CARDS_AMOUNT)) { + break; + } + + player->addPreyCards(cardsAmount); + + success = true; + break; + } + + case OfferTypes_t::CHARM_EXPANSION: { + if (player->hasCharmExpansion()) { + break; + } + + player->setCharmExpansion(true); + + success = true; + break; + } + + case OfferTypes_t::HUNTINGSLOT: { + const auto &thirdSlot = player->getTaskHuntingSlotById(PreySlot_Three); + + if (thirdSlot->state != PreyTaskDataState_Locked) { + break; + } + + thirdSlot->eraseTask(); + thirdSlot->reloadReward(); + thirdSlot->state = PreyTaskDataState_Selection; + thirdSlot->reloadMonsterGrid(player->getTaskHuntingBlackList(), player->getLevel()); + player->reloadTaskSlot(PreySlot_Three); + + success = true; + break; + } + + case OfferTypes_t::INSTANT_REWARD_ACCESS: { + auto offerInstantAmount = offer->getCount(); + auto playerInstantAmount = static_cast(player->getStorageValue(STORAGEVALUE_REWARD_ACCESS)); + + auto instantLimit = static_cast(g_configManager().getNumber(INSTANT_DAILY_REWARD_ACCESS_AMOUNT)); + if (playerInstantAmount + offerInstantAmount >= instantLimit) { + break; + } + + player->addStorageValue(STORAGEVALUE_REWARD_ACCESS, playerInstantAmount + offerInstantAmount); + + success = true; + break; + } + + case OfferTypes_t::HIRELING: + g_logger().warn("HIRELING"); + break; + case OfferTypes_t::HIRELING_NAMECHANGE: + g_logger().warn("HIRELING_NAMECHANGE"); + break; + case OfferTypes_t::HIRELING_SEXCHANGE: + g_logger().warn("HIRELING_SEXCHANGE"); + break; + case OfferTypes_t::HIRELING_SKILL: + g_logger().warn("HIRELING_SKILL"); + break; + case OfferTypes_t::HIRELING_OUTFIT: + g_logger().warn("HIRELING_OUTFIT"); + break; + + default: + break; + } + + if (success) { + uint32_t offerPrice = offer->getPrice(); + + if (offer->getType() == OfferTypes_t::EXPBOOST) { + offerPrice = calculateBoostPrice(player->getStorageValue(STORAGEVALUE_EXPBOOST) - 1); + } + + std::string returnmessage; + if (offer->getType() == OfferTypes_t::NAMECHANGE) { + returnmessage = "Thank you for your purchase! To finalise the Character Name Change, please start your client anew. Note that you cannot enter houses or open doors anymore which are still labelled with your old character name until the responsible character invited you with your new name."; + } else { + returnmessage = fmt::format("You have purchased {} for {} coins.", offer->getName(), offerPrice); + } + auto result = player->getAccount()->removeCoins(CoinType::Transferable, offerPrice, returnmessage); + if (result == AccountErrors_t::RemoveCoins) { + player->sendStoreError(StoreErrors_t::PURCHASE, "You don't have enough coins."); + return; + } + + player->sendStoreSuccess(returnmessage); + + auto offerAmount = offer->getCount(); + auto pricePerItem = offerPrice ? offerPrice / offerAmount : 0; + g_logger().trace("[{}] offer price {}, offer ammount {}, price per item {}", __FUNCTION__, offerPrice, offerAmount, pricePerItem); + player->addStoreHistory(false, player->getName(), getTimeNow(), offerPrice, StoreDetailType::Finished, MARKETACTION_SELL, offer->getName()); + } else { + player->sendStoreError(StoreErrors_t::PURCHASE, errorMessage); + } + + player->updateUIExhausted(); + player->openStore(); +} + bool Game::tryRetrieveStashItems(const std::shared_ptr &player, const std::shared_ptr &item) { ObjectCategory_t category = getObjectCategory(item); return internalCollectManagedItems(player, item, category, false) == RETURNVALUE_NOERROR; diff --git a/src/game/game.hpp b/src/game/game.hpp index ff2e127fd25..85c83b722ea 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -49,6 +49,7 @@ class ContainerIterator; class Item; class BedItem; class WildcardTreeNode; +class Offer; struct Achievement; struct HighscoreCategory; @@ -56,6 +57,7 @@ struct TextMessage; enum ObjectCategory_t : uint8_t; enum class ForgeAction_t : uint8_t; +enum class Blessings : uint8_t; using CreatureVector = std::vector>; @@ -276,7 +278,17 @@ class Game { void playerRequestInventoryImbuements(uint32_t playerId, bool isTrackerOpen); - bool addItemStoreInbox(const std::shared_ptr &player, uint32_t itemId); + // Store Functions + void playerOpenStore(uint32_t playerId); + void playerCoinTransfer(uint32_t playerId, const std::string &receptorName, uint32_t coinAmount); + void playerOpenStoreHistory(uint32_t playerId, uint32_t page); + void playerBuyStoreOffer(uint32_t playerId, const Offer* offer, std::string newName, uint8_t sexId); + // Process Offers + bool processChargesOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t charges = 0, bool movable = false); + bool processStackableOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t amount = 1, bool movable = false); + bool processHouseOffer(const std::shared_ptr &player, uint32_t itemId, uint16_t charges = 0); + bool processNameChangeOffer(const std::shared_ptr &player, std::string name); + bool processTempleOffer(const std::shared_ptr &player); void playerRewardChestCollect(uint32_t playerId, const Position &pos, uint16_t itemId, uint8_t stackPos, uint32_t maxMoveItems = 0); @@ -372,7 +384,7 @@ class Game { void playerClearImbuement(uint32_t playerid, uint8_t slot); void playerCloseImbuementWindow(uint32_t playerid); void playerTurn(uint32_t playerId, Direction dir); - void playerRequestOutfit(uint32_t playerId); + void playerRequestOutfit(uint32_t playerId, uint16_t tryOutfit = 0, uint16_t tryMount = 0); void playerShowQuestLog(uint32_t playerId); void playerShowQuestLine(uint32_t playerId, uint16_t questId); void playerSay(uint32_t playerId, uint16_t channelId, SpeakClasses type, const std::string &receiver, const std::string &text); diff --git a/src/io/CMakeLists.txt b/src/io/CMakeLists.txt index d9785acdc08..82dc321eb2c 100644 --- a/src/io/CMakeLists.txt +++ b/src/io/CMakeLists.txt @@ -4,6 +4,7 @@ target_sources(${PROJECT_NAME}_lib PRIVATE io_wheel.cpp iobestiary.cpp io_bosstiary.cpp + io_store.cpp ioguild.cpp iologindata.cpp functions/iologindata_load_player.cpp diff --git a/src/io/functions/iologindata_load_player.cpp b/src/io/functions/iologindata_load_player.cpp index e724d0ceda9..5919b583db6 100644 --- a/src/io/functions/iologindata_load_player.cpp +++ b/src/io/functions/iologindata_load_player.cpp @@ -26,6 +26,7 @@ #include "enums/object_category.hpp" #include "game/game.hpp" #include "io/ioguild.hpp" +#include "io/io_store.hpp" #include "io/ioprey.hpp" #include "items/containers/depot/depotchest.hpp" #include "items/containers/inbox/inbox.hpp" @@ -889,6 +890,29 @@ void IOLoginDataLoad::loadPlayerForgeHistory(const std::shared_ptr &play } } +void IOLoginDataLoad::loadPlayerStoreHistory(const std::shared_ptr &player, DBResult_ptr result) { + if (!result || !player) { + g_logger().warn("[IOLoginData::loadPlayer] - Player or Result nullptr: {}", __FUNCTION__); + return; + } + + std::string query = fmt::format("SELECT * FROM `store_history` WHERE `account_id` = {}", player->getAccountId()); + if ((result = Database::getInstance().storeQuery(query))) { + do { + StoreHistory history; + history.description = result->getString("description"); + history.coinAmount = result->getNumber("coin_amount"); + history.coinType = result->getNumber("coin_type"); + history.type = result->getNumber("type"); + history.createdAt = result->getNumber("created_at"); + history.playerName = result->getString("player_name"); + history.totalPrice = result->getNumber("total_price"); + history.fromMarket = result->getNumber("show_detail"); + player->setStoreHistory(history); + } while (result->next()); + } +} + void IOLoginDataLoad::loadPlayerBosstiary(const std::shared_ptr &player, DBResult_ptr result) { if (!result) { g_logger().warn("[{}] - Result nullptr", __FUNCTION__); diff --git a/src/io/functions/iologindata_load_player.hpp b/src/io/functions/iologindata_load_player.hpp index ca05bdcb1b8..2f108be3487 100644 --- a/src/io/functions/iologindata_load_player.hpp +++ b/src/io/functions/iologindata_load_player.hpp @@ -40,6 +40,7 @@ class IOLoginDataLoad : public IOLoginData { static void loadPlayerPreyClass(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerTaskHuntingClass(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerForgeHistory(const std::shared_ptr &player, DBResult_ptr result); + static void loadPlayerStoreHistory(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerBosstiary(const std::shared_ptr &player, DBResult_ptr result); static void loadPlayerInitializeSystem(const std::shared_ptr &player); static void loadPlayerUpdateSystem(const std::shared_ptr &player); diff --git a/src/io/functions/iologindata_save_player.cpp b/src/io/functions/iologindata_save_player.cpp index 613fdac1cb0..d54b018a15f 100644 --- a/src/io/functions/iologindata_save_player.cpp +++ b/src/io/functions/iologindata_save_player.cpp @@ -13,6 +13,7 @@ #include "creatures/combat/condition.hpp" #include "creatures/monsters/monsters.hpp" #include "game/game.hpp" +#include "io/io_store.hpp" #include "io/ioprey.hpp" #include "items/containers/depot/depotchest.hpp" #include "items/containers/inbox/inbox.hpp" @@ -178,10 +179,16 @@ bool IOLoginDataSave::savePlayerFirst(const std::shared_ptr &player) { return db.executeQuery(query.str()); } + // Change Name Store + std::string playerName = player->getNewName(); + if (playerName.empty()) { + playerName = player->getName(); + } + // First, an UPDATE query to write the player itself query.str(""); query << "UPDATE `players` SET "; - query << "`name` = " << db.escapeString(player->name) << ","; + query << "`name` = " << db.escapeString(playerName) << ","; query << "`level` = " << player->level << ","; query << "`group_id` = " << player->group->id << ","; query << "`vocation` = " << player->getVocationId() << ","; @@ -726,6 +733,46 @@ bool IOLoginDataSave::savePlayerForgeHistory(const std::shared_ptr &play return true; } +bool IOLoginDataSave::savePlayerStoreHistory(const std::shared_ptr &player) { + if (!player) { + g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); + return false; + } + + std::ostringstream query; + query << "DELETE FROM `store_history` WHERE `account_id` = " << player->getAccountId(); + if (!Database::getInstance().executeQuery(query.str())) { + return false; + } + + query.str(""); + DBInsert insertQuery("INSERT INTO `store_history` (`account_id`, `description`, `coin_amount`, `coin_type`, `type`, `created_at`, `player_name`, `total_price`, `show_detail`) VALUES"); + for (const auto &historyEntry : player->getStoreHistory()) { + const auto descriptionString = Database::getInstance().escapeString(historyEntry.description); + const auto playerNameString = Database::getInstance().escapeString(historyEntry.playerName); + // Append query informations + query << player->getAccountId() << ',' + << descriptionString << ',' + << historyEntry.coinAmount << ',' + << static_cast(historyEntry.coinType) << ',' + << static_cast(historyEntry.type) << ',' + << historyEntry.createdAt << ',' + << playerNameString << ',' + << historyEntry.totalPrice << ',' + << historyEntry.fromMarket; + + if (!insertQuery.addRow(query)) { + return false; + } + } + + if (!insertQuery.execute()) { + return false; + } + + return true; +} + bool IOLoginDataSave::savePlayerBosstiary(const std::shared_ptr &player) { if (!player) { g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__); diff --git a/src/io/functions/iologindata_save_player.hpp b/src/io/functions/iologindata_save_player.hpp index 1f871a3b5db..40e95236689 100644 --- a/src/io/functions/iologindata_save_player.hpp +++ b/src/io/functions/iologindata_save_player.hpp @@ -28,6 +28,7 @@ class IOLoginDataSave : public IOLoginData { static bool savePlayerPreyClass(const std::shared_ptr &player); static bool savePlayerTaskHuntingClass(const std::shared_ptr &player); static bool savePlayerForgeHistory(const std::shared_ptr &player); + static bool savePlayerStoreHistory(const std::shared_ptr &player); static bool savePlayerBosstiary(const std::shared_ptr &player); static bool savePlayerStorage(const std::shared_ptr &player); diff --git a/src/io/io_bosstiary.cpp b/src/io/io_bosstiary.cpp index 3e8a05524e6..297f2ab3132 100644 --- a/src/io/io_bosstiary.cpp +++ b/src/io/io_bosstiary.cpp @@ -198,7 +198,7 @@ void IOBosstiary::addBosstiaryKill(const std::shared_ptr &player, const int32_t value = player->getStorageValue(STORAGEVALUE_PODIUM); if (value != 1 && newBossLevel == 2) { - auto returnValue = g_game().addItemStoreInbox(player, ITEM_PODIUM_OF_VIGOUR); + auto returnValue = g_game().processHouseOffer(player, ITEM_PODIUM_OF_VIGOUR); if (!returnValue) { return; } diff --git a/src/io/io_store.cpp b/src/io/io_store.cpp new file mode 100644 index 00000000000..f70c6bfbdc5 --- /dev/null +++ b/src/io/io_store.cpp @@ -0,0 +1,526 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#include "io/io_store.hpp" + +#include "config/configmanager.hpp" +#include "database/databasetasks.hpp" +#include "creatures/monsters/monsters.hpp" +#include "creatures/players/player.hpp" +#include "utils/tools.hpp" + +const std::unordered_map IOStore::stringToOfferTypeMap = { + { "item", OfferTypes_t::ITEM }, + { "stackable", OfferTypes_t::STACKABLE }, + { "charges", OfferTypes_t::CHARGES }, + { "looktype", OfferTypes_t::LOOKTYPE }, + { "outfit", OfferTypes_t::OUTFIT }, + { "mount", OfferTypes_t::MOUNT }, + { "nameChange", OfferTypes_t::NAMECHANGE }, + { "sexChange", OfferTypes_t::SEXCHANGE }, + { "house", OfferTypes_t::HOUSE }, + { "expBoost", OfferTypes_t::EXPBOOST }, + { "preySlot", OfferTypes_t::PREYSLOT }, + { "preyBonus", OfferTypes_t::PREYBONUS }, + { "temple", OfferTypes_t::TEMPLE }, + { "blessings", OfferTypes_t::BLESSINGS }, + { "allblessings", OfferTypes_t::ALLBLESSINGS }, + { "premium", OfferTypes_t::PREMIUM }, + { "pouch", OfferTypes_t::POUCH }, + { "instantReward", OfferTypes_t::INSTANT_REWARD_ACCESS }, + { "charmExpansion", OfferTypes_t::CHARM_EXPANSION }, + { "huntingSlot", OfferTypes_t::HUNTINGSLOT }, + { "hireling", OfferTypes_t::HIRELING }, + { "hirelingNameChange", OfferTypes_t::HIRELING_NAMECHANGE }, + { "hirelingSexChange", OfferTypes_t::HIRELING_SEXCHANGE }, + { "hirelingSkill", OfferTypes_t::HIRELING_SKILL }, + { "hirelingOutfit", OfferTypes_t::HIRELING_OUTFIT } +}; + +const std::unordered_map IOStore::offersDisableIndex = { + { OfferTypes_t::OUTFIT, 0 }, + { OfferTypes_t::MOUNT, 1 }, + { OfferTypes_t::EXPBOOST, 2 }, + { OfferTypes_t::PREYSLOT, 3 }, + { OfferTypes_t::HUNTINGSLOT, 3 }, + { OfferTypes_t::PREYBONUS, 4 }, + { OfferTypes_t::BLESSINGS, 5 }, + { OfferTypes_t::ALLBLESSINGS, 6 }, + { OfferTypes_t::POUCH, 7 }, + { OfferTypes_t::INSTANT_REWARD_ACCESS, 8 }, + { OfferTypes_t::CHARM_EXPANSION, 9 }, + { OfferTypes_t::TEMPLE, 10 } +}; + +const std::unordered_map IOStore::stringToOfferStateMap = { + { "none", States_t::NONE }, { "new", States_t::NEW }, { "sale", States_t::SALE }, { "timed", States_t::TIMED } +}; + +const std::unordered_map IOStore::stringToBannerTypeMap = { + { "collection", BannerType::COLLECTION }, + { "offer", BannerType::OFFER } +}; + +bool IOStore::loadFromXml() { + pugi::xml_document doc; + auto folder = g_configManager().getString(CORE_DIRECTORY) + "/XML/store/store.xml"; + pugi::xml_parse_result result = doc.load_file(folder.c_str()); + if (!result) { + printXMLError(__FUNCTION__, folder, result); + return false; + } + + auto storeNode = doc.child("store"); + + pugi::xml_node homeNode = storeNode.child("home"); + if (!loadStoreHome(homeNode)) { + return false; + } + + for (pugi::xml_node category : storeNode.children("category")) { + auto newCategory = loadCategoryFromXml(category); + + pugi::xml_node child = category.first_child(); + if (child && std::string(child.name()) == "subcategory") { + for (pugi::xml_node subcategory : category.children("subcategory")) { + auto newSubCategory = loadCategoryFromXml(subcategory, true); + + for (pugi::xml_node offer : subcategory.children("offer")) { + if (!loadOfferFromXml(&newSubCategory, offer)) { + return false; + } + } + m_subCategoryVector.push_back(newSubCategory); + newCategory.addSubCategory(newSubCategory); + } + } else if (child && std::string(child.name()) == "offer") { + newCategory.setSpecialCategory(true); + for (pugi::xml_node offer : category.children("offer")) { + if (!loadOfferFromXml(&newCategory, offer)) { + return false; + } + } + } + addCategory(newCategory); + } + + return true; +} + +Category IOStore::loadCategoryFromXml(const pugi::xml_node &category, bool isSubCategory /* = false*/) { + auto categoryName = std::string(category.attribute("name").as_string()); + auto categoryIcon = std::string(category.attribute("icon").as_string()); + auto categoryRookString = std::string(category.attribute("rookgaard").as_string()); + + bool categoryRook = categoryRookString == "yes"; + + if (isSubCategory) { + auto subCategoryStateString = std::string(category.attribute("state").as_string()); + States_t subCategoryState = States_t::NONE; + if (auto it = stringToOfferStateMap.find(subCategoryStateString); + it != stringToOfferStateMap.end()) { + subCategoryState = it->second; + } + + Category newSubCategory(categoryName, categoryIcon, categoryRook, subCategoryState); + return newSubCategory; + } + + Category newCategory(categoryName, categoryIcon, categoryRook); + return newCategory; +} + +bool IOStore::loadOfferFromXml(Category* category, const pugi::xml_node &offer) { + const std::string &name = offer.attribute("name").as_string(); + if (name.empty()) { + g_logger().warn("Offer name empty."); + return false; + } + + uint32_t id = 0; + auto offerId = offer.attribute("id"); + auto maleOutfitId = offer.attribute("male"); + if (offerId) { + id = static_cast(offerId.as_uint()); + } else if (maleOutfitId) { + id = static_cast(maleOutfitId.as_uint()); + } else { + id = dynamicId; + dynamicId++; + } + + Offer newOffer(id, name); + + uint32_t price = offer.attribute("price").as_uint(); + if (price == 0) { + g_logger().warn("Offer {} price is 0.", name); + return false; + } + newOffer.m_price = price; + + const std::string &typeString = offer.attribute("type").as_string(); + OfferTypes_t type = OfferTypes_t::NONE; + if (auto it = stringToOfferTypeMap.find(typeString); + it != stringToOfferTypeMap.end()) { + type = it->second; + } else { + g_logger().warn("Offer {} type is none.", name); + return false; + } + newOffer.m_type = type; + + switch (type) { + using enum OfferTypes_t; + + case OUTFIT: + case HIRELING: { + newOffer.m_outfitId.femaleId = static_cast(offer.attribute("female").as_uint()); + newOffer.m_outfitId.maleId = static_cast(maleOutfitId.as_uint()); + break; + } + + case ITEM: + case HOUSE: + case POUCH: + case CHARGES: + case STACKABLE: { + newOffer.m_itemId = static_cast(offer.attribute("item").as_uint()); + break; + } + + default: + break; + } + + if (offer.attribute("count")) { + newOffer.m_count = static_cast(offer.attribute("count").as_uint()); + } else if (offer.attribute("charges")) { + newOffer.m_count = static_cast(offer.attribute("charges").as_uint()); + } + + const std::string &stateString = offer.attribute("state").as_string("none"); + if (auto it = stringToOfferStateMap.find(stateString); + it != stringToOfferStateMap.end()) { + newOffer.m_state = it->second; + } + + const std::string &coinTypeString = offer.attribute("coinType").as_string("normal"); + const std::string &collection = offer.attribute("collection").as_string(); + + newOffer.m_icon = offer.attribute("icon").as_string(); + newOffer.m_description = offer.attribute("description").as_string(); + newOffer.m_movable = offer.attribute("movable").as_bool(); + newOffer.m_validUntil = static_cast(offer.attribute("validUntil").as_uint()); + newOffer.m_collectionName = collection; + newOffer.m_coinType = coinTypeString == "normal" ? CoinType::Normal : CoinType::Transferable; + newOffer.m_parentName = category->getName(); + + auto baseOffer = getOfferByName(name); + if (baseOffer) { + baseOffer->addRelatedOffer(newOffer); + addOffer(id, newOffer); + return true; + } + newOffer.m_relatedOffers.push_back(newOffer); + addOffer(id, newOffer); + + const Offer* foundOffer = getOfferById(id); + if (!foundOffer) { + g_logger().warn("Offer {} not found.", name); + return false; + } + + category->addOffer(foundOffer); + + if (!collection.empty()) { + category->addCollection(collection); + } + + return true; +} + +bool IOStore::loadStoreHome(const pugi::xml_node &homeNode) { + auto bannersNode = homeNode.child("banners"); + m_bannerDelay = static_cast(bannersNode.attribute("delay").as_uint(5)); + + pugi::xml_node bannersChild = bannersNode.first_child(); + if (bannersChild && std::string(bannersChild.name()) == "banner") { + for (pugi::xml_node banner : bannersNode.children("banner")) { + BannerInfo tempBanner; + + std::string bannerPath = std::string(banner.attribute("path").as_string()); + if (bannerPath.empty()) { + return false; + } + tempBanner.path = bannerPath; + + std::string bannerTypeString = std::string(banner.attribute("type").as_string()); + auto it = stringToBannerTypeMap.find(bannerTypeString); + if (it == stringToBannerTypeMap.end()) { + return false; + } + BannerType bannerType = it->second; + + if (bannerType == BannerType::COLLECTION) { + std::string categoryName = std::string(banner.attribute("category").as_string()); + std::string collectionName = std::string(banner.attribute("collection").as_string()); + if (categoryName.empty() || collectionName.empty()) { + return false; + } + tempBanner.categoryName = categoryName; + tempBanner.collectionName = collectionName; + } else if (bannerType == BannerType::OFFER) { + std::string offerName = std::string(banner.attribute("offer").as_string()); + if (offerName.empty()) { + return false; + } + tempBanner.offerName = offerName; + } else { + return false; + } + tempBanner.type = bannerType; + + m_banners.push_back(tempBanner); + } + } else { + return false; + } + + auto homeOffersNode = homeNode.child("offers"); + pugi::xml_node homeOffersChild = homeOffersNode.first_child(); + if (homeOffersChild && std::string(homeOffersChild.name()) == "offer") { + for (pugi::xml_node offer : homeOffersNode.children("offer")) { + const std::string &homeOfferName = offer.attribute("name").as_string(); + m_homeOffers.push_back(homeOfferName); + } + } + + return true; +} + +const std::vector &IOStore::getCategoryVector() const { + return m_categoryVector; +} +void IOStore::addCategory(const Category &newCategory) { + for (const auto &category : m_categoryVector) { + if (newCategory.getName() == category.getName()) { + return; + } + } + m_categoryVector.push_back(newCategory); +} + +const Category* IOStore::getCategoryByName(std::string_view categoryName) const { + for (const auto &category : m_categoryVector) { + if (categoryName == category.getName()) { + return &category; + } + } + return nullptr; +} + +const Category* IOStore::getSubCategoryByName(std::string_view subCategoryName) const { + for (const auto &subCategory : m_subCategoryVector) { + if (subCategoryName == subCategory.getName()) { + return &subCategory; + } + } + return nullptr; +} + +void IOStore::addOffer(uint32_t offerId, Offer offer) { + auto it = m_offersMap.find(offerId); + if (it != m_offersMap.end()) { + return; + } + + m_offersMap.try_emplace(offerId, std::move(offer)); +} + +const Offer* IOStore::getOfferById(uint32_t offerId) const { + if (auto it = m_offersMap.find(offerId); + it != m_offersMap.end()) { + return &it->second; + } + + return nullptr; +} + +std::vector IOStore::getOffersContainingSubstring(const std::string &searchString) { + std::vector offersVector; + const auto &lowerSearchString = asLowerCaseString(searchString); + + for (const auto &[id, offer] : m_offersMap) { + const auto ¤tOfferName = offer.getName(); + const auto &lowerCurrentOfferName = asLowerCaseString(currentOfferName); + + if (lowerCurrentOfferName.find(lowerSearchString) != std::string::npos) { + offersVector.push_back(offer); + } + } + + return offersVector; +} + +Offer* IOStore::getOfferByName(const std::string &searchString) { + const auto &lowerSearchString = asLowerCaseString(searchString); + + for (auto &[id, offer] : m_offersMap) { + const auto ¤tOfferName = offer.getName(); + const auto &lowerCurrentOfferName = asLowerCaseString(currentOfferName); + + if (lowerSearchString == lowerCurrentOfferName) { + return &offer; + } + } + + return nullptr; +} + +const std::vector &IOStore::getBannersVector() const { + return m_banners; +} +const std::vector &IOStore::getHomeOffersVector() const { + return m_homeOffers; +} +uint32_t IOStore::getBannerDelay() const { + return m_bannerDelay; +} + +const Category* IOStore::findCategory(const std::string &categoryName) const { + auto currentCategory = getCategoryByName(categoryName); + if (!currentCategory) { + currentCategory = getSubCategoryByName(categoryName); + return currentCategory; + } + + if (currentCategory->isSpecialCategory()) { + return currentCategory; + } + + auto subCat = currentCategory->getFirstSubCategory(); + return subCat; +} + +const std::vector &IOStore::getOffersDisableReasonVector() const { + return m_offersDisableReason; +} + +StoreHistoryDetail IOStore::getStoreHistoryDetail(const std::string &playerName, uint32_t createdAt, bool hasDetail) { + std::string query = fmt::format( + "SELECT * FROM `store_history` WHERE `player_name` = {} AND `created_at` = {} AND `show_detail` = {}", + g_database().escapeString(playerName), createdAt, static_cast(hasDetail) + ); + + DBResult_ptr result = Database::getInstance().storeQuery(query); + if (!result) { + g_logger().error("Failed to get store history details."); + return {}; + } + + StoreHistoryDetail storeDetail; + storeDetail.type = result->getNumber("type"); + storeDetail.createdAt = createdAt; + storeDetail.coinAmount = result->getNumber("coin_amount"); + storeDetail.description = result->getString("description"); + storeDetail.playerName = result->getString("player_name"); + storeDetail.totalPrice = result->getNumber("total_price"); + + g_logger().debug("Store details for creation data: {}, description '{}', player '{}', coin amount '{}', total price '{}'", storeDetail.createdAt, storeDetail.description, storeDetail.playerName, storeDetail.coinAmount, storeDetail.totalPrice); + return storeDetail; +} + +// Category Class functions +const Category* Category::getFirstSubCategory() const { + return &m_subCategories.at(0); +} +const std::vector &Category::getSubCategoriesVector() const { + return m_subCategories; +} +void Category::addSubCategory(const Category &newSubCategory) { + for (const auto &subCategory : m_subCategories) { + if (newSubCategory.getName() == subCategory.getName()) { + return; + } + } + m_subCategories.push_back(newSubCategory); +} + +const std::vector &Category::getOffersVector() const { + return m_offers; +} +void Category::addOffer(const Offer* newOffer) { + for (const auto &offer : m_offers) { + if (newOffer->getID() == offer->getID() + && newOffer->getName() == offer->getName() + && newOffer->getCount() == offer->getCount()) { + return; + } + } + m_offers.push_back(newOffer); +} + +const std::vector &Category::getCollectionsVector() const { + return m_collections; +} +void Category::addCollection(const std::string &newCollection) { + for (const auto &collection : m_collections) { + if (newCollection == collection) { + return; + } + } + m_collections.push_back(newCollection); +} + +// Offer Functions +const std::vector &Offer::getRelatedOffersVector() const { + return m_relatedOffers; +} +void Offer::addRelatedOffer(const Offer &relatedOffer) { + for (const auto &offer : m_relatedOffers) { + if (relatedOffer.getCount() == offer.getCount() + && relatedOffer.getPrice() == offer.getPrice()) { + return; + } + } + + m_relatedOffers.push_back(relatedOffer); +} + +ConverType_t Offer::getConverType() const { + using enum OfferTypes_t; + + switch (m_type) { + case MOUNT: + return ConverType_t::MOUNT; + case LOOKTYPE: + return ConverType_t::LOOKTYPE; + case ITEM: + case STACKABLE: + case HOUSE: + case CHARGES: + case POUCH: + return ConverType_t::ITEM; + case OUTFIT: + case HIRELING: + return ConverType_t::OUTFIT; + + default: + return ConverType_t::NONE; + } +} + +bool Offer::getUseConfigure() const { + using enum OfferTypes_t; + if (m_type == NAMECHANGE || m_type == HIRELING || m_type == HIRELING_NAMECHANGE) { + return true; + } + + return false; +} diff --git a/src/io/io_store.hpp b/src/io/io_store.hpp new file mode 100644 index 00000000000..ddb1d1f8625 --- /dev/null +++ b/src/io/io_store.hpp @@ -0,0 +1,268 @@ +/** + * Canary - A free and open-source MMORPG server emulator + * Copyright (©) 2019-2022 OpenTibiaBR + * Repository: https://github.com/opentibiabr/canary + * License: https://github.com/opentibiabr/canary/blob/main/LICENSE + * Contributors: https://github.com/opentibiabr/canary/graphs/contributors + * Website: https://docs.opentibiabr.org/ + */ + +#pragma once + +#include "enums/account_coins.hpp" +#include "enums/player_store_enums.hpp" + +// Structs +struct OutfitIds { + uint16_t maleId {}; + uint16_t femaleId {}; +}; + +struct BannerInfo { + std::string path; + BannerType type; + + std::string categoryName; + std::string collectionName; + + std::string offerName; +}; + +struct StoreHistoryDetail { + StoreDetailType type {}; + uint32_t createdAt {}; + int32_t coinAmount {}; + int64_t totalPrice {}; + std::string description {}; + std::string playerName {}; +}; + +struct RelatedOffer { + uint32_t id {}; + uint32_t price {}; + uint16_t count {}; +}; + +struct StoreHistory { + time_t createdAt {}; + + int32_t coinAmount {}; + CoinType coinType {}; + StoreDetailType type {}; + int64_t totalPrice {}; + + std::string description {}; + std::string playerName {}; + bool fromMarket = false; +}; + +class Category; +class Offer; + +class IOStore { +public: + IOStore(const IOStore &) = delete; + IOStore &operator=(const IOStore &) = delete; + IOStore(IOStore &&) = delete; + IOStore &operator=(IOStore &&) = delete; + + static IOStore &getInstance() { + static IOStore instance; + return instance; + } + + bool loadFromXml(); + + static const std::unordered_map stringToOfferTypeMap; + static const std::unordered_map offersDisableIndex; + static const std::unordered_map stringToOfferStateMap; + static const std::unordered_map stringToBannerTypeMap; + + const std::vector &getCategoryVector() const; + const Category* getCategoryByName(std::string_view categoryName) const; + const Category* getSubCategoryByName(std::string_view subCategoryName) const; + const Offer* getOfferById(uint32_t offerId) const; + + const std::vector &getBannersVector() const; + const std::vector &getHomeOffersVector() const; + uint32_t getBannerDelay() const; + + const Category* findCategory(const std::string &categoryName) const; + + const std::vector &getOffersDisableReasonVector() const; + + std::vector getOffersContainingSubstring(const std::string &searchString); + Offer* getOfferByName(const std::string &searchString); + + static StoreHistoryDetail getStoreHistoryDetail(const std::string &playerName, uint32_t createdAt, bool hasDetail); + +private: + IOStore() = default; + ~IOStore() = default; + + uint32_t dynamicId = std::numeric_limits::max(); + + // Home + std::vector m_banners; + uint8_t m_bannerDelay = 5; + std::vector m_homeOffers; + + std::vector m_categoryVector; + std::vector m_subCategoryVector; + std::map m_offersMap; + + Category loadCategoryFromXml(const pugi::xml_node &offer, bool isSubCategory = false); + bool loadOfferFromXml(Category* category, const pugi::xml_node &offer); + bool loadStoreHome(const pugi::xml_node &homeNode); + + void addCategory(const Category &newCategory); + void addOffer(uint32_t offerId, Offer offer); + + std::vector m_offersDisableReason = { + "You already own this outfit.", + "You already own this mount.", + "You can't buy XP Boost for today.", + "You already have 3 slots released.", + "You already have maximum of prey wildcards.", + "You reached the maximum amount for this blessing.", + "You reached the maximum amount for some blessing.", + "You already have a pouch.", + "You already have maximum of reward tokens.", + "You already have charm expansion.", + "You cannot make this purchase as long as your characters has a logout block." + }; +}; + +class Category { // We're gonna use the same class for Category and Subcategory +public: + Category(const std::string &name, const std::string &icon, bool rookgaard, States_t state = States_t::NONE) : + m_name(name), m_icon(icon), m_canRookgaardAccess(rookgaard), m_state(state) { } + + const std::string &getName() const { + return m_name; + } + const std::string &getIcon() const { + return m_icon; + } + States_t getState() const { + return m_state; + } + bool canRookgaardAccess() const { + return m_canRookgaardAccess; + } + bool isSpecialCategory() const { + return m_specialCategory; + } + + const Category* getFirstSubCategory() const; + const std::vector &getSubCategoriesVector() const; + const std::vector &getOffersVector() const; + const std::vector &getCollectionsVector() const; + +private: + friend class IOStore; + + std::string m_name; + std::string m_icon; + bool m_canRookgaardAccess; + States_t m_state = States_t::NONE; + bool m_specialCategory = false; + + // Used when Category class is a Category + std::vector m_subCategories; + + // Used when Category class is a Subcategory or a "Special Category" + std::vector m_offers; + std::vector m_collections; + + void addSubCategory(const Category &newSubCategory); + void addCollection(const std::string &newCollection); + void addOffer(const Offer* newOffer); + void setSpecialCategory(bool state) { + m_specialCategory = state; + } +}; + +class Offer { +public: + Offer(uint32_t id, const std::string &name) : + m_id(id), m_name(name) { } + + const std::string &getName() const { + return m_name; + } + const std::string &getIcon() const { + return m_icon; + } + uint32_t getID() const { + return m_id; + } + uint32_t getPrice() const { + return m_price; + } + uint32_t getItemId() const { + return m_itemId; + } + OfferTypes_t getType() const { + return m_type; + } + States_t getState() const { + return m_state; + } + uint16_t getCount() const { + return m_count; + } + uint16_t getValidUntil() const { + return m_validUntil; + } + CoinType getCoinType() const { + return m_coinType; + } + std::string getDescription() const { + return m_description; + } + OutfitIds getOutfitIds() const { + return m_outfitId; + } + std::string getCollection() const { + return m_collectionName; + } + std::string getParentName() const { + return m_parentName; + } + ConverType_t getConverType() const; + bool getUseConfigure() const; + bool isMovable() const { + return m_movable; + } + + const std::vector &getRelatedOffersVector() const; + void addRelatedOffer(const Offer &relatedOffer); + +private: + friend class IOStore; + + // Mandatory + uint32_t m_id; + std::string m_name; + uint32_t m_price; + OfferTypes_t m_type = OfferTypes_t::NONE; + + // Optional + std::string m_icon; + States_t m_state = States_t::NONE; + uint32_t m_itemId = 0; + uint16_t m_count = 1; // Or charges + uint16_t m_validUntil; + CoinType m_coinType = CoinType::Normal; + std::string m_description; + OutfitIds m_outfitId; + std::string m_collectionName; + bool m_movable; + + // Internal + std::string m_parentName; + std::vector m_relatedOffers; +}; + +constexpr auto g_ioStore = IOStore::getInstance; diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index c0a6a13f363..6e7dd9b8fab 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -171,6 +171,9 @@ bool IOLoginData::loadPlayer(const std::shared_ptr &player, const DBResu // load forge history IOLoginDataLoad::loadPlayerForgeHistory(player, result); + // load store history + IOLoginDataLoad::loadPlayerStoreHistory(player, result); + // load bosstiary IOLoginDataLoad::loadPlayerBosstiary(player, result); @@ -258,6 +261,10 @@ bool IOLoginData::savePlayerGuard(const std::shared_ptr &player) { throw DatabaseException("[IOLoginDataSave::savePlayerForgeHistory] - Failed to save player forge history: " + player->getName()); } + if (!IOLoginDataSave::savePlayerStoreHistory(player)) { + throw DatabaseException("[IOLoginDataSave::savePlayerStoreHistory] - Failed to save player store history: " + player->getName()); + } + if (!IOLoginDataSave::savePlayerBosstiary(player)) { throw DatabaseException("[IOLoginDataSave::savePlayerBosstiary] - Failed to save player bosstiary: " + player->getName()); } diff --git a/src/io/iomarket.cpp b/src/io/iomarket.cpp index fe91b3f6d94..89c9a9bba2e 100644 --- a/src/io/iomarket.cpp +++ b/src/io/iomarket.cpp @@ -164,6 +164,7 @@ void IOMarket::processExpiredOffers(const DBResult_ptr &result, bool) { const auto playerId = result->getNumber("player_id"); const auto amount = result->getNumber("amount"); auto tier = getTierFromDatabaseTable(result->getString("tier")); + auto timestamp = result->getNumber("created"); if (result->getNumber("sale") == 1) { const ItemType &itemType = Item::items[result->getNumber("itemtype")]; if (itemType.id == 0) { @@ -214,6 +215,11 @@ void IOMarket::processExpiredOffers(const DBResult_ptr &result, bool) { } } + if (itemType.id == ITEM_STORE_COIN) { + auto description = "Sell Offer Cancelled or Expired"; + player->addStoreDetail(description, amount, timestamp); + } + if (player->isOffline()) { g_saveManager().savePlayer(player); } @@ -234,7 +240,7 @@ void IOMarket::checkExpiredOffers() { const time_t lastExpireDate = getTimeNow() - g_configManager().getNumber(MARKET_OFFER_DURATION); std::ostringstream query; - query << "SELECT `id`, `amount`, `price`, `itemtype`, `player_id`, `sale`, `tier` FROM `market_offers` WHERE `created` <= " << lastExpireDate; + query << "SELECT * FROM `market_offers` WHERE `created` <= " << lastExpireDate; g_databaseTasks().store(query.str(), IOMarket::processExpiredOffers); int32_t checkExpiredMarketOffersEachMinutes = g_configManager().getNumber(CHECK_EXPIRED_MARKET_OFFERS_EACH_MINUTES); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index dc02e316f1f..9b8a2c5eb5b 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -22,6 +22,7 @@ #include "creatures/players/achievement/player_achievement.hpp" #include "creatures/players/cyclopedia/player_badge.hpp" #include "creatures/players/cyclopedia/player_cyclopedia.hpp" +#include "creatures/players/gamestore/player_store_detail.hpp" #include "creatures/players/grouping/familiars.hpp" #include "creatures/players/grouping/party.hpp" #include "creatures/players/grouping/team_finder.hpp" @@ -41,6 +42,7 @@ #include "io/iobestiary.hpp" #include "io/iologindata.hpp" #include "io/iomarket.hpp" +#include "io/io_store.hpp" #include "io/ioprey.hpp" #include "items/items_classification.hpp" #include "items/weapons/weapons.hpp" @@ -1251,7 +1253,8 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xC0: parseForgeBrowseHistory(msg); break; - case 0xC9: /* update tile */ + case 0xC9: + parseStoreDetail(msg); break; case 0xCA: parseUpdateContainer(msg); @@ -1269,7 +1272,7 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby sendBlessingWindow(); break; case 0xD2: - g_game().playerRequestOutfit(player->getID()); + parseGetOutfit(msg); break; case 0xD3: parseSetOutfit(msg); @@ -1331,8 +1334,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xEE: parseGreet(msg); break; - // Premium coins transfer - // case 0xEF: parseCoinTransfer(msg); break; + case 0xEF: + parseCoinTransfer(msg); + break; case 0xF0: g_game().playerShowQuestLog(player->getID()); break; @@ -1363,13 +1367,22 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xFF: parseRewardChestCollect(msg); break; - // case 0xFA: parseStoreOpen(msg); break; - // case 0xFB: parseStoreRequestOffers(msg); break; - // case 0xFC: parseStoreBuyOffer(msg) break; - // case 0xFD: parseStoreOpenTransactionHistory(msg); break; - // case 0xFE: parseStoreRequestTransactionHistory(msg); break; - - // case 0xDF, 0xE0, 0xE1, 0xFB, 0xFC, 0xFD, 0xFE Premium Shop. + case 0xFA: + parseOpenStore(); + break; + case 0xFB: + parseRequestStoreOffers(msg); + break; + case 0xFC: + parseBuyStoreOffer(msg); + break; + case 0xFD: + parseOpenStoreHistory(msg); + break; + case 0xFE: + parseRequestStoreHistory(msg); + break; + // case 0xDF, 0xE0, 0xE1 default: std::string hexString = fmt::format("0x{:02x}", recvbyte); @@ -1657,6 +1670,26 @@ void ProtocolGame::parseAutoWalk(NetworkMessage &msg) { g_game().playerAutoWalk(player->getID(), path); } +void ProtocolGame::parseGetOutfit(NetworkMessage &msg) { + auto eventType = msg.getByte(); + if (eventType == 1) { + auto tryOnId = msg.get(); + const auto outfit = Outfits::getInstance().getOutfitByLookType(player, tryOnId); + if (outfit) { + g_game().playerRequestOutfit(player->getID(), tryOnId, 0); + return; + } + + const auto mount = g_game().mounts->getMountByClientID(tryOnId); + if (mount) { + g_game().playerRequestOutfit(player->getID(), 0, tryOnId); + return; + } + } + + g_game().playerRequestOutfit(player->getID()); +} + void ProtocolGame::parseSetOutfit(NetworkMessage &msg) { if (!player || player->isRemoved()) { return; @@ -3040,11 +3073,6 @@ void ProtocolGame::parseGreet(NetworkMessage &msg) { g_game().playerNpcGreet(player->getID(), npcId); } -void ProtocolGame::parseOfferDescription(NetworkMessage &msg) { - auto offerId = msg.get(); - g_logger().debug("[{}] offer id: {}", __FUNCTION__, offerId); -} - void ProtocolGame::parsePreyAction(NetworkMessage &msg) { int8_t index = -1; uint8_t slot = msg.getByte(); @@ -5078,11 +5106,11 @@ void ProtocolGame::sendCoinBalance() { msg.addByte(0xDF); msg.addByte(0x01); - msg.add(player->coinBalance); // Normal Coins + msg.add(player->coinBalance + player->coinTransferableBalance); // Total Coins (Normal Coins + Transferable Coins) msg.add(player->coinTransferableBalance); // Transferable Coins if (!oldProtocol) { - msg.add(player->coinBalance); // Reserved Auction Coins + msg.add(0x00); // Reserved Auction Coins } writeToOutputBuffer(msg); @@ -7117,7 +7145,7 @@ void ProtocolGame::sendHouseWindow(uint32_t windowTextId, const std::string &tex writeToOutputBuffer(msg); } -void ProtocolGame::sendOutfitWindow() { +void ProtocolGame::sendOutfitWindow(uint16_t tryOutfit, uint16_t tryMount) { NetworkMessage msg; msg.addByte(0xC8); @@ -7125,8 +7153,18 @@ void ProtocolGame::sendOutfitWindow() { auto isSupportOutfit = player->isWearingSupportOutfit(); bool mounted = false; + if (tryOutfit != 0) { + currentOutfit.lookType = tryOutfit; + currentOutfit.lookAddons = tryOutfit >= 962 && tryOutfit <= 975 ? 0 : 3; + } + if (!isSupportOutfit) { - const auto currentMount = g_game().mounts->getMountByID(player->getLastMount()); + std::shared_ptr currentMount; + if (tryMount == 0) { + currentMount = g_game().mounts->getMountByID(player->getLastMount()); + } else { + currentMount = g_game().mounts->getMountByClientID(tryMount); + } if (currentMount) { mounted = (currentOutfit.lookMount == currentMount->clientId); currentOutfit.lookMount = currentMount->clientId; @@ -7216,34 +7254,31 @@ void ProtocolGame::sendOutfitWindow() { for (const auto &outfit : outfits) { uint8_t addons; - if (player->getOutfitAddons(outfit, addons)) { + if (tryOutfit == 0 && player->getOutfitAddons(outfit, addons)) { msg.add(outfit->lookType); msg.addString(outfit->name); msg.addByte(addons); msg.addByte(0x00); ++outfitSize; } else if (outfit->lookType == 1210 || outfit->lookType == 1211) { - if (player->canWear(1210, 0) || player->canWear(1211, 0)) { - msg.add(outfit->lookType); - msg.addString(outfit->name); - msg.addByte(3); - msg.addByte(0x02); - ++outfitSize; - } + msg.add(outfit->lookType); + msg.addString(outfit->name); + msg.addByte(3); + msg.addByte(0x02); + ++outfitSize; } else if (outfit->lookType == 1456 || outfit->lookType == 1457) { - if (player->canWear(1456, 0) || player->canWear(1457, 0)) { - msg.add(outfit->lookType); - msg.addString(outfit->name); - msg.addByte(3); - msg.addByte(0x03); - ++outfitSize; - } + msg.add(outfit->lookType); + msg.addString(outfit->name); + msg.addByte(3); + msg.addByte(0x03); + ++outfitSize; } else if (outfit->from == "store") { msg.add(outfit->lookType); msg.addString(outfit->name); msg.addByte(outfit->lookType >= 962 && outfit->lookType <= 975 ? 0 : 3); msg.addByte(0x01); - msg.add(0x00); + uint32_t offerId = player->getSex() == 1 ? outfit->lookType : (outfit->lookType - 1); + msg.add(offerId); ++outfitSize; } @@ -7273,7 +7308,7 @@ void ProtocolGame::sendOutfitWindow() { msg.add(mount->clientId); msg.addString(mount->name); msg.addByte(0x01); - msg.add(0x00); + msg.add(mount->id); ++mountSize; } @@ -7312,11 +7347,21 @@ void ProtocolGame::sendOutfitWindow() { msg.add(familiarSize); msg.setBufferPosition(endFamiliars); - msg.addByte(0x00); // Try outfit + // Try on + if (tryOutfit != 0) { + msg.addByte(0x01); + } else if (tryMount != 0) { + msg.addByte(0x02); + } else { + msg.addByte(0x00); + } + msg.addByte(mounted ? 0x01 : 0x00); - // Version 12.81 - Random mount 'bool' - msg.addByte(isSupportOutfit ? 0x00 : (player->isRandomMounted() ? 0x01 : 0x00)); + // // Version 12.81 - Random mount 'bool' + if (tryOutfit == 0 && tryMount == 0) { + msg.addByte(isSupportOutfit ? 0x00 : (player->isRandomMounted() ? 0x01 : 0x00)); + } writeToOutputBuffer(msg); } @@ -9224,6 +9269,513 @@ void ProtocolGame::sendBosstiaryEntryChanged(uint32_t bossid) { writeToOutputBuffer(msg); } +void ProtocolGame::parseOpenStore() { + g_game().playerOpenStore(player->getID()); +} + +void ProtocolGame::openStore() { + if (!player || oldProtocol) { + return; + } + + NetworkMessage msg; + msg.addByte(0xFB); + + auto startCategories = msg.getBufferPosition(); + uint16_t totalCategories = 0; + msg.skipBytes(2); + + auto storeCategories = g_ioStore().getCategoryVector(); + auto playerVocationId = player->getVocationId(); + // Categories Bytes + for (const auto &category : storeCategories) { + if (!category.canRookgaardAccess() && playerVocationId == 0) { + continue; + } + msg.addString(category.getName()); + + auto categoryState = magic_enum::enum_integer(category.getState()); + msg.addByte(categoryState); + + msg.addByte(0x01); // Category Icons Amounts + msg.addString(category.getIcon()); + + msg.add(0x00); // Parent + ++totalCategories; + } + + // Subcategories Bytes + for (const auto &category : storeCategories) { + if (category.isSpecialCategory()) { + continue; + } + + const auto &internalSubCatVector = category.getSubCategoriesVector(); + for (const auto &subCategory : internalSubCatVector) { + if (!subCategory.canRookgaardAccess() && playerVocationId == 0) { + continue; + } + msg.addString(subCategory.getName()); + + auto subCategoryState = magic_enum::enum_integer(subCategory.getState()); + msg.addByte(subCategoryState); + + msg.addByte(0x01); // Category Icons Amounts + msg.addString(subCategory.getIcon()); + + msg.addString(category.getName()); // Parent + ++totalCategories; + } + } + msg.setBufferPosition(startCategories); + msg.add(totalCategories); + + writeToOutputBuffer(msg); + updateCoinBalance(); +} + +void ProtocolGame::parseOfferDescription(NetworkMessage &msg) { + uint32_t offerId = msg.get(); + auto offer = g_ioStore().getOfferById(offerId); + if (!offer) { + return; + } + + sendOfferDescription(offer); +} + +void ProtocolGame::parseCoinTransfer(NetworkMessage &msg) { + auto receptor = msg.getString(); + auto amount = msg.get(); + + g_game().playerCoinTransfer(player->getID(), receptor, amount); +} + +void ProtocolGame::parseOpenStoreHistory(NetworkMessage &msg) { + msg.getByte(); // Entry Pages = Always 26? + g_game().playerOpenStoreHistory(player->getID(), 1); +} + +void ProtocolGame::parseRequestStoreHistory(NetworkMessage &msg) { + uint32_t currentPage = msg.get(); + g_game().playerOpenStoreHistory(player->getID(), currentPage + 1); +} + +void ProtocolGame::sendStoreHistory(uint32_t page) { + uint16_t entriesPerPage = 26; + auto historyVector = player->getStoreHistory(); + auto historyVectorLen = getVectorIterationIncreaseCount(historyVector); + + uint32_t lastPage = (1 < std::floor((historyVectorLen - 1) / entriesPerPage) + 1) ? static_cast(std::floor((historyVectorLen - 1) / entriesPerPage) + 1) : 1; + uint32_t currentPage = (lastPage < page) ? lastPage : page; + + std::vector historyPerPage; + uint16_t pageFirstEntry = (0 < historyVectorLen - (currentPage - 1) * entriesPerPage) ? historyVectorLen - (currentPage - 1) * entriesPerPage : 0; + uint16_t pageLastEntry = currentPage != lastPage ? historyVectorLen - currentPage * entriesPerPage : 0; + for (uint16_t entry = pageFirstEntry; entry > pageLastEntry; --entry) { + historyPerPage.push_back(historyVector[entry - 1]); + } + auto historyPageToSend = getVectorIterationIncreaseCount(historyPerPage); + + NetworkMessage msg; + msg.addByte(0xFD); + msg.add(currentPage - 1); // Current page + msg.add(lastPage); // Last page + msg.addByte(static_cast(historyPageToSend)); // History to send + + if (historyPageToSend > 0) { + for (const auto &history : historyPerPage) { + msg.add(history.fromMarket ? history.createdAt : 0); + msg.add(history.createdAt); + msg.addByte(uint8_t()); // HistoryType_t enum + msg.add(history.coinAmount); + + msg.addByte(enumToValue(history.coinType)); + msg.addString(history.description); + msg.addByte(history.fromMarket); // Toggle details button + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::parseRequestStoreOffers(NetworkMessage &msg) { + uint8_t actionType = msg.getByte(); + + if (actionType == 0) { + sendStoreHome(); + } else if (actionType == 1) { + uint8_t innerAction = msg.getByte(); + if (innerAction == 0) { + auto currentCategory = g_ioStore().findCategory("Premium Time"); + if (!currentCategory) { + return; + } + sendCategoryOffers(currentCategory); + } else if (innerAction == 1) { + auto currentCategory = g_ioStore().findCategory("Boosts"); + if (!currentCategory) { + return; + } + sendCategoryOffers(currentCategory); + } + } else if (actionType == 2) { + std::string categoryName = msg.getString(); + std::string collectionName = msg.getString(); + + auto currentCategory = g_ioStore().findCategory(categoryName); + if (!currentCategory) { + return; + } + + if (!collectionName.empty()) { + const auto &collectionsVector = currentCategory->getCollectionsVector(); + if (std::find(collectionsVector.begin(), collectionsVector.end(), collectionName) == collectionsVector.end()) { + collectionName.clear(); + } + } + + sendCategoryOffers(currentCategory, 0, collectionName); + } else if (actionType == 3) { + uint8_t innerAction = msg.getByte(); + + uint32_t offerId = 0; + std::string categoryName = "Useful Things"; + + if (innerAction >= 4 && innerAction <= 10) { + categoryName = "Blessings"; + offerId = innerAction - 2; + } else { + switch (innerAction) { + case 0: + offerId = 15; + break; + case 1: + offerId = 11; + break; + case 3: + offerId = 13; + break; + case 13: + offerId = 14; + break; + default: + offerId = 0; + break; + } + } + + auto currentCategory = g_ioStore().findCategory(categoryName); + if (!currentCategory) { + return; + } + sendCategoryOffers(currentCategory, offerId); + } else if (actionType == 4) { + uint32_t offerId = msg.get(); + auto offer = g_ioStore().getOfferById(offerId); + if (!offer) { + return; + } + auto parentName = offer->getParentName(); + auto parentCategory = g_ioStore().findCategory(parentName); + if (!parentCategory) { + return; + } + sendCategoryOffers(parentCategory, offerId); + } else if (actionType == 5) { + std::string searchName = msg.getString(); + auto offersVector = g_ioStore().getOffersContainingSubstring(searchName); + sendFoundOffers(offersVector); + } +} + +void ProtocolGame::sendOfferBytes(NetworkMessage &msg, const Offer* offer) { + msg.addString(offer->getName()); + const auto &relatedOffersVector = offer->getRelatedOffersVector(); + auto offersCount = getVectorIterationIncreaseCount(relatedOffersVector); + msg.addByte(static_cast(offersCount)); // Related Offers inside a Base Offer + sendOfferDescription(offer); + for (const auto &relatedOffer : relatedOffersVector) { + msg.add(relatedOffer.getID()); + msg.add(relatedOffer.getCount()); + + uint32_t offerPrice = relatedOffer.getPrice(); + if (offer->getType() == OfferTypes_t::EXPBOOST) { + offerPrice = calculateBoostPrice(player->getStorageValue(STORAGEVALUE_EXPBOOST)); + } + msg.add(offerPrice); + + auto offerCoinType = magic_enum::enum_integer(relatedOffer.getCoinType()); + msg.addByte(offerCoinType); // Coin Type + + auto canBuyOffer = player->canBuyStoreOffer(&relatedOffer); + msg.addByte(canBuyOffer ? 0x00 : 0x01); // Disabled (Bool) + if (!canBuyOffer) { + msg.addByte(0x01); + auto vectorIndex = g_ioStore().offersDisableIndex.find(relatedOffer.getType()); + msg.add(vectorIndex->second); + } + + msg.addByte(0x00); // Offer State + } + + uint8_t tryOn = 0; + auto offerConverType = magic_enum::enum_integer(offer->getConverType()); + + msg.addByte(offerConverType); // ConverType + if (offerConverType == 0) { // Normal + msg.addString(offer->getIcon()); + } else if (offerConverType == 1) { // Mount + auto offerMount = g_game().mounts->getMountByID(offer->getID()); + msg.add(offerMount->clientId); + tryOn = 1; + } else if (offerConverType == 2) { // Outfit + auto playerSex = player->getSex(); + auto offerOutfitIds = offer->getOutfitIds(); + msg.add(playerSex == PLAYERSEX_FEMALE ? offerOutfitIds.femaleId : offerOutfitIds.maleId); + auto playerOutfit = player->getCurrentOutfit(); + msg.addByte(playerOutfit.lookHead); + msg.addByte(playerOutfit.lookBody); + msg.addByte(playerOutfit.lookLegs); + msg.addByte(playerOutfit.lookFeet); + tryOn = 1; + } else if (offerConverType == 3) { // Item + msg.add(static_cast(offer->getItemId())); + } else if (offerConverType == 4) { // Male/Female Outfit + auto playerSex = player->getSex(); + auto offerOutfitIds = offer->getOutfitIds(); + msg.addByte(playerSex == PLAYERSEX_FEMALE ? 2 : 1); + msg.add(offerOutfitIds.maleId); + msg.add(offerOutfitIds.femaleId); + auto playerOutfit = player->getCurrentOutfit(); + msg.addByte(playerOutfit.lookHead); + msg.addByte(playerOutfit.lookBody); + msg.addByte(playerOutfit.lookLegs); + msg.addByte(playerOutfit.lookFeet); + tryOn = 1; + } + + msg.addByte(tryOn); // Try on Type + msg.addString(offer->getCollection()); // Collection + msg.add(0x00); // Popularity Score + msg.add(0x00); // State New Until + + msg.addByte(offer->getUseConfigure()); + msg.add(0x00); // Products Capacity +} + +void ProtocolGame::sendStoreHome() { + NetworkMessage msg; + msg.addByte(0xFC); + msg.addString("Home"); + + msg.add(0x00); + msg.addByte(0x03); // Window Type + msg.addByte(0x00); // Collection Size + msg.add(0x00); // Collection Name + + auto disableReasonVector = g_ioStore().getOffersDisableReasonVector(); + uint16_t disableReasonVectorLen = disableReasonVector.size(); + msg.add(disableReasonVectorLen); // Disable Reasons Vector Length + if (disableReasonVectorLen > 0) { + for (const auto &reason : disableReasonVector) { + msg.addString(reason); + } + } + + // Offer Bytes + auto homeOffersVector = g_ioStore().getHomeOffersVector(); + auto homeOffersCount = getVectorIterationIncreaseCount(homeOffersVector); + msg.add(homeOffersCount); // Offers Amount + if (homeOffersCount > 0) { + for (const auto &homeOfferName : homeOffersVector) { + const Offer* offer = g_ioStore().getOfferByName(homeOfferName); + if (!offer) { + continue; + } + sendOfferBytes(msg, offer); + } + } + + // Banner Bytes + auto bannersVector = g_ioStore().getBannersVector(); + auto bannersVectorSize = bannersVector.size(); + msg.addByte(bannersVectorSize); // Banners Amount + for (const auto &banner : bannersVector) { + msg.addString(banner.path); + msg.addByte(enumToValue(banner.type)); // Banner Type (0x02 = Collection, 0x04 = Offer) + if (banner.type == BannerType::COLLECTION) { + msg.addString(banner.categoryName); + msg.addString(banner.collectionName); + } else if (banner.type == BannerType::OFFER) { + const auto &offer = g_ioStore().getOfferByName(banner.offerName); + if (!offer) { + continue; + } + msg.add(offer->getID()); // Offer Id + } + msg.addByte(0x00); // Unknown + msg.addByte(0x00); // Unknown + } + + auto bannerDelay = g_ioStore().getBannerDelay(); + msg.addByte(bannerDelay); // Banner Delay + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendCategoryOffers(const Category* category, uint32_t redirectId /* = 0*/, std::string collectionRedirect /* = ""*/) { + NetworkMessage msg; + msg.addByte(0xFC); + + msg.addString(category->getName()); + + msg.add(redirectId); + + msg.addByte(0x00); // Window Type + + const auto &collectionsVector = category->getCollectionsVector(); + const auto collectionsVectorSize = collectionsVector.size(); + msg.addByte(collectionsVectorSize); // Collection Size + if (collectionsVectorSize > 0) { + for (const auto &collectionName : collectionsVector) { + msg.addString(collectionName); // Collection Name + } + } + + msg.addString(collectionRedirect); + + auto disableReasonVector = g_ioStore().getOffersDisableReasonVector(); + uint16_t disableReasonVectorLen = disableReasonVector.size(); + msg.add(disableReasonVectorLen); // Disable Reasons Vector Length + if (disableReasonVectorLen > 0) { + for (const auto &reason : disableReasonVector) { + msg.addString(reason); + } + } + + auto offersVector = category->getOffersVector(); + auto offersCount = getVectorIterationIncreaseCount(offersVector); + msg.add(offersCount); + + if (offersCount > 0) { + for (const auto &offer : offersVector) { + sendOfferBytes(msg, offer); + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendFoundOffers(std::vector foundOffers) { + NetworkMessage msg; + msg.addByte(0xFC); + + msg.addString("Search"); + + msg.add(0x00); + msg.addByte(0x00); // Window Type + msg.addByte(0x00); // Collection Size + msg.addString(""); // Collection Name + + auto disableReasonVector = g_ioStore().getOffersDisableReasonVector(); + uint16_t disableReasonVectorLen = disableReasonVector.size(); + msg.add(disableReasonVectorLen); // Disable Reasons Vector Length + if (disableReasonVectorLen > 0) { + for (auto reason : disableReasonVector) { + msg.addString(reason); + } + } + + auto offersCount = getVectorIterationIncreaseCount(foundOffers); + msg.add(offersCount); + + if (offersCount > 0) { + for (const auto &offer : foundOffers) { + sendOfferBytes(msg, &offer); + } + } + + msg.addByte(0x00); + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendOfferDescription(const Offer* offer) { + NetworkMessage msg; + msg.addByte(0xEA); + + msg.add(offer->getID()); + msg.addString(offer->getDescription()); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::parseBuyStoreOffer(NetworkMessage &msg) { + auto offerId = msg.get(); + msg.getByte(); // Service Type + + const auto* currentOffer = g_ioStore().getOfferById(offerId); + if (!currentOffer) { + g_logger().error("Offer with id {} was not found returning nullptr", offerId); + player->sendStoreError(StoreErrors_t::PURCHASE, "An error has occurred, please contact your administrator."); + return; + } + + auto currentOfferType = currentOffer->getType(); + + std::string stringName = ""; + uint8_t sexId = 0; + + if (currentOfferType == OfferTypes_t::NAMECHANGE + || currentOfferType == OfferTypes_t::HIRELING_NAMECHANGE) { + stringName = msg.getString(); + if (stringName.empty()) { + requestPurchaseData(currentOffer->getID(), 1); + return; + } + } else if (currentOfferType == OfferTypes_t::HIRELING) { + stringName = msg.getString(); + sexId = msg.getByte(); + if (stringName.empty()) { + requestPurchaseData(currentOffer->getID(), 3); + return; + } + } + + g_game().playerBuyStoreOffer(player->getID(), currentOffer, stringName, sexId); +} + +void ProtocolGame::sendStoreSuccess(std::string successMessage) { + NetworkMessage msg; + msg.addByte(0xFE); + + msg.addByte(0x00); + msg.addString(successMessage); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendStoreError(StoreErrors_t errorType, std::string errorMessage) { + NetworkMessage msg; + msg.addByte(0xE0); + + uint8_t errorNum = magic_enum::enum_integer(errorType); + msg.addByte(errorNum); + msg.addString(errorMessage); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::requestPurchaseData(uint32_t offerId, uint8_t offerType) { + NetworkMessage msg; + msg.addByte(0xE1); + + msg.add(offerId); + msg.addByte(offerType); // 1 or 3 + + writeToOutputBuffer(msg); +} + void ProtocolGame::sendSingleSoundEffect(const Position &pos, SoundEffect_t id, SourceEffect_t source) { if (oldProtocol) { return; @@ -9304,6 +9856,82 @@ void ProtocolGame::parseSaveWheel(NetworkMessage &msg) { g_game().playerSaveWheel(player->getID(), msg); } +void ProtocolGame::parseStoreDetail(NetworkMessage &msg) { + if (!player || oldProtocol) { + return; + } + + auto createdAt = msg.get(); + if (createdAt != 0) { + // Get the offer by creation data + auto storeDetail = g_ioStore().getStoreHistoryDetail(player->getName(), createdAt, true); + if (storeDetail.createdAt != createdAt) { + g_logger().error("Store detail not found for creation data: {}, player '{}'", createdAt, player->getName()); + return; + } + + sendStoreDetail(storeDetail); + } +} + +void ProtocolGame::sendStoreDetail(const StoreHistoryDetail &storeHistoryDetail) { + if (!player || oldProtocol) { + return; + } + + auto pricePerCoin = storeHistoryDetail.totalPrice ? storeHistoryDetail.totalPrice / storeHistoryDetail.coinAmount : 0; + + NetworkMessage newMsg; + newMsg.addByte(0xCB); + newMsg.add(storeHistoryDetail.createdAt); // Offer id (We will use the creation date as there will only be one for each offer) + newMsg.addByte(enumToValue(storeHistoryDetail.type)); // Type (0 = buy, 1 = created) + if (storeHistoryDetail.type == StoreDetailType::Finished) { + newMsg.add(storeHistoryDetail.createdAt); // Creation data + newMsg.addString(storeHistoryDetail.description); + newMsg.addString(storeHistoryDetail.playerName); + newMsg.add(storeHistoryDetail.coinAmount); + newMsg.add(pricePerCoin); + newMsg.add(storeHistoryDetail.totalPrice); + } else { + auto storeDetailScoped = player->getStoreDetailScope(storeHistoryDetail.createdAt); + auto coinAmountOpt = storeDetailScoped->get("sold-coin-amount"); + int32_t coinAmount = 0; + if (coinAmountOpt) { + coinAmount = coinAmountOpt->getNumber(); + } + + auto receivedGoldAmountOpt = storeDetailScoped->get("received-gold-amount"); + int64_t receivedGoldAmount = 0; + if (receivedGoldAmountOpt) { + receivedGoldAmount = receivedGoldAmountOpt->getNumber(); + } + + uint32_t stillInMarket = std::abs(storeHistoryDetail.coinAmount) - coinAmount; + newMsg.addString(storeHistoryDetail.playerName); // Character name + newMsg.add(storeHistoryDetail.coinAmount); // Total offer coin amount + newMsg.add(coinAmount); // Sold tibia coins + newMsg.add(stillInMarket); // Tibia coins still in market + newMsg.add(pricePerCoin); // Piece price + newMsg.add(receivedGoldAmount); // Received gold coins + // Obter todos os detalhes do histórico para a oferta específica + auto details = player->getStoreHistoryDetails(storeHistoryDetail.createdAt); + // Add the sorted details to the NetworkMessage + newMsg.add(details.size()); // Number of details + for (const auto &[createdAt, detail] : details) { + newMsg.add(static_cast(std::stoll(createdAt))); // Creation date + newMsg.addString(detail.description); + newMsg.addByte(detail.isGold); // Enum 0 - 1 (0 = tibia coin, 1 = gold coin) + newMsg.add(detail.coinAmount); + + // Log the sent action + g_logger().debug("[{}] - player: {}, {}", __FUNCTION__, player->getName(), detail.toString()); + } + } + + g_logger().debug("sendStoreDetail: CreatedAt: {}, Type: {}, Description: {}, PlayerName: {}, CoinAmount: {}, TotalPrice: {}", storeHistoryDetail.createdAt, storeHistoryDetail.type, storeHistoryDetail.description, storeHistoryDetail.playerName, storeHistoryDetail.coinAmount, storeHistoryDetail.totalPrice); + writeToOutputBuffer(newMsg); +} + void ProtocolGame::sendDisableLoginMusic() { if (oldProtocol || !player || player->getOperatingSystem() >= CLIENTOS_OTCLIENT_LINUX) { return; diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 2048bea7c16..d0d2450c2b1 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -29,6 +29,7 @@ enum Slots_t : uint8_t; enum CombatType_t : uint8_t; enum SoundEffect_t : uint16_t; enum class SourceEffect_t : uint8_t; +enum class StoreErrors_t : uint8_t; class NetworkMessage; class Player; @@ -47,6 +48,8 @@ class Party; class Creature; class MonsterType; class Npc; +class Offer; +class Category; struct ModalWindow; struct Position; @@ -59,6 +62,8 @@ struct ShopBlock; struct MarketOfferEx; struct HistoryMarketOffer; struct LightInfo; +struct StoreDetail; +struct StoreHistoryDetail; using ProtocolGame_ptr = std::shared_ptr; using ItemVector = std::vector>; @@ -133,6 +138,7 @@ class ProtocolGame final : public Protocol { // Parse methods void parseAutoWalk(NetworkMessage &msg); + void parseGetOutfit(NetworkMessage &msg); void parseSetOutfit(NetworkMessage &msg); void parseSay(NetworkMessage &msg); void parseLookAt(NetworkMessage &msg); @@ -174,7 +180,6 @@ class ProtocolGame final : public Protocol { void parseGreet(NetworkMessage &msg); void parseBugReport(NetworkMessage &msg); - void parseOfferDescription(NetworkMessage &msg); void parsePreyAction(NetworkMessage &msg); void parseSendResourceBalance(); void parseRuleViolationReport(NetworkMessage &msg); @@ -304,6 +309,27 @@ class ProtocolGame final : public Protocol { void sendBosstiaryCooldownTimer(); void sendBosstiaryEntryChanged(uint32_t bossid); + // Store Functions + void parseOpenStore(); + void parseOfferDescription(NetworkMessage &msg); + void parseCoinTransfer(NetworkMessage &msg); + void parseRequestStoreOffers(NetworkMessage &msg); + void parseBuyStoreOffer(NetworkMessage &msg); + void parseOpenStoreHistory(NetworkMessage &msg); + void parseRequestStoreHistory(NetworkMessage &msg); + + void openStore(); + void sendStoreHome(); + void sendOfferBytes(NetworkMessage &msg, const Offer* offer); + void sendCategoryOffers(const Category* category, uint32_t redirectId = 0, std::string collectionRedirect = ""); + void sendFoundOffers(std::vector foundOffers); + void sendOfferDescription(const Offer* offer); + void sendStoreHistory(uint32_t page); + void sendStoreSuccess(std::string successMessage); + void sendStoreError(StoreErrors_t errorType, std::string errorMessage); + void requestPurchaseData(uint32_t offerId, uint8_t offerType); + // End Store Functions + void sendAllowBugReport(); void sendDistanceShoot(const Position &from, const Position &to, uint16_t type); void sendMagicEffect(const Position &pos, uint16_t type); @@ -382,7 +408,7 @@ class ProtocolGame final : public Protocol { void sendTextWindow(uint32_t windowTextId, uint32_t itemId, const std::string &text); void sendTextWindow(uint32_t windowTextId, const std::shared_ptr &item, uint16_t maxlen, bool canWrite); void sendHouseWindow(uint32_t windowTextId, const std::string &text); - void sendOutfitWindow(); + void sendOutfitWindow(uint16_t tryOutfit, uint16_t tryMount); void sendPodiumWindow(const std::shared_ptr &podium, const Position &position, uint16_t itemId, uint8_t stackpos); void sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus); @@ -506,6 +532,8 @@ class ProtocolGame final : public Protocol { void sendOpenWheelWindow(uint32_t ownerId); void parseSaveWheel(NetworkMessage &msg); void parseWheelGemAction(NetworkMessage &msg); + void parseStoreDetail(NetworkMessage &msg); + void sendStoreDetail(const StoreHistoryDetail &storeDetail); friend class Player; friend class PlayerWheel; diff --git a/src/utils/const.hpp b/src/utils/const.hpp index 4cc296e4a7e..2d97359d230 100644 --- a/src/utils/const.hpp +++ b/src/utils/const.hpp @@ -25,8 +25,13 @@ static constexpr int32_t CHANNEL_PRIVATE = 0xFFFF; static constexpr int32_t EVENT_IMBUEMENT_INTERVAL = 1000; static constexpr uint8_t IMBUEMENT_MAX_TIER = 3; +static constexpr uint8_t STORE_BLESSING_MAX_AMOUNT = 5; + +static constexpr int32_t STORAGEVALUE_REWARD_ACCESS = 14901; +static constexpr int32_t STORAGEVALUE_POUCH = 14904; static constexpr int32_t STORAGEVALUE_EMOTE = 30008; static constexpr int32_t STORAGEVALUE_PODIUM = 30020; +static constexpr int32_t STORAGEVALUE_EXPBOOST = 51052; static constexpr int32_t STORAGEVALUE_BESTIARYKILLCOUNT = 61305000; // Can get up to 2000 storages! // Hazard system storage diff --git a/src/utils/tools.cpp b/src/utils/tools.cpp index 57c9c873282..26a1674f8c5 100644 --- a/src/utils/tools.cpp +++ b/src/utils/tools.cpp @@ -1669,10 +1669,39 @@ NameEval_t validateName(const std::string &name) { return VALID; } +bool isKegItem(uint16_t itemId) { + return itemId >= ITEM_KEG_START && itemId <= ITEM_KEG_END; +} + bool isCaskItem(uint16_t itemId) { return (itemId >= ITEM_HEALTH_CASK_START && itemId <= ITEM_HEALTH_CASK_END) || (itemId >= ITEM_MANA_CASK_START && itemId <= ITEM_MANA_CASK_END) || (itemId >= ITEM_SPIRIT_CASK_START && itemId <= ITEM_SPIRIT_CASK_END); } +bool isExerciseWeapon(uint16_t itemId) { + return (itemId >= ITEM_EXERCISE_START && itemId <= ITEM_EXERCISE_END) || (itemId >= ITEM_NEW_EXERCISE_START && itemId <= ITEM_NEW_EXERCISE_END); +} + +uint32_t calculateBoostPrice(int32_t boostCounter) { + if (boostCounter > 5) { + return 360; + } + + switch (boostCounter) { + case 1: + return 30; + case 2: + return 45; + case 3: + return 90; + case 4: + return 180; + case 5: + return 360; + default: + return 30; + } +} + std::string getObjectCategoryName(ObjectCategory_t category) { switch (category) { case OBJECTCATEGORY_ARMORS: diff --git a/src/utils/tools.hpp b/src/utils/tools.hpp index 64fd2e8c1e6..e12488b3621 100644 --- a/src/utils/tools.hpp +++ b/src/utils/tools.hpp @@ -159,7 +159,10 @@ void capitalizeWordsIgnoringString(std::string &source, const std::string &strin void consoleHandlerExit(); NameEval_t validateName(const std::string &name); +bool isKegItem(uint16_t itemId); bool isCaskItem(uint16_t itemId); +bool isExerciseWeapon(uint16_t itemId); +uint32_t calculateBoostPrice(int32_t boostCounter); std::string getObjectCategoryName(ObjectCategory_t category); bool isValidObjectCategory(ObjectCategory_t category); diff --git a/src/utils/utils_definitions.hpp b/src/utils/utils_definitions.hpp index 6eae663daca..27eb401e0d3 100644 --- a/src/utils/utils_definitions.hpp +++ b/src/utils/utils_definitions.hpp @@ -566,6 +566,9 @@ enum ItemID_t : uint16_t { ITEM_EXERCISE_START = 28552, ITEM_EXERCISE_END = 28557, + ITEM_NEW_EXERCISE_START = 35279, + ITEM_NEW_EXERCISE_END = 35290, + /** Casks and Kegs **/ ITEM_HEALTH_CASK_START = 25879, ITEM_HEALTH_CASK_END = 25883, diff --git a/tests/fixture/account/in_memory_account_repository.hpp b/tests/fixture/account/in_memory_account_repository.hpp index d86d6a483be..1f7e0a29c0c 100644 --- a/tests/fixture/account/in_memory_account_repository.hpp +++ b/tests/fixture/account/in_memory_account_repository.hpp @@ -120,6 +120,24 @@ namespace tests { return true; } + bool registerStoreTransaction( + const uint32_t &id, + CoinTransactionType type, + uint32_t amount, + CoinType coinType, + const std::string &description, + const time_t &time + ) final { + auto accountCoins = storeTransactions_.find(id); + + if (accountCoins == storeTransactions_.end()) { + storeTransactions_[id] = std::vector>(); + } + + storeTransactions_[id].emplace_back(type, amount, coinType, description, time); + return true; + } + bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) final { for (auto it = accounts.begin(); it != accounts.end(); ++it) { if (it->second.id == id) { @@ -135,6 +153,7 @@ namespace tests { accounts.clear(); coins_.clear(); coinsTransactions_.clear(); + storeTransactions_.clear(); failSave = false; failAddCoins = false; failGetPassword = false; @@ -152,6 +171,7 @@ namespace tests { phmap::flat_hash_map accounts; phmap::flat_hash_map> coins_; phmap::flat_hash_map>> coinsTransactions_; + phmap::flat_hash_map>> storeTransactions_; }; }