forgottenserver/data/lib/core/item.lua

782 lines
21 KiB
Lua

function Item.getType(self)
return ItemType(self:getId())
end
function Item:getClassification()
return self:getType():getClassification()
end
function Item.isContainer(self)
return false
end
function Item.isCreature(self)
return false
end
function Item.isMonster(self)
return false
end
function Item.isNpc(self)
return false
end
function Item.isPlayer(self)
return false
end
function Item.isTeleport(self)
return false
end
function Item.isPodium(self)
return false
end
function Item.isTile(self)
return false
end
function Item:isItemType()
return false
end
do
local aux = {
["Defense"] = {key = ITEM_ATTRIBUTE_DEFENSE},
["ExtraDefense"] = {key = ITEM_ATTRIBUTE_EXTRADEFENSE},
["Attack"] = {key = ITEM_ATTRIBUTE_ATTACK},
["AttackSpeed"] = {key = ITEM_ATTRIBUTE_ATTACK_SPEED},
["HitChance"] = {key = ITEM_ATTRIBUTE_HITCHANCE},
["ShootRange"] = {key = ITEM_ATTRIBUTE_SHOOTRANGE},
["Armor"] = {key = ITEM_ATTRIBUTE_ARMOR},
["Duration"] = {key = ITEM_ATTRIBUTE_DURATION, cmp = function(v) return v > 0 end},
["Text"] = {key = ITEM_ATTRIBUTE_TEXT, cmp = function(v) return v ~= "" end},
["Date"] = {key = ITEM_ATTRIBUTE_DATE},
["Writer"] = {key = ITEM_ATTRIBUTE_WRITER, cmp = function(v) return v ~= "" end}
}
function setAuxFunctions()
for name, def in pairs(aux) do
Item["get" .. name] = function(self)
local attr = self:getAttribute(def.key)
if def.cmp and def.cmp(attr) then
return attr
elseif not def.cmp and attr and attr ~= 0 then
return attr
end
local default = ItemType["get" .. name]
return default and default(self:getType()) or nil
end
end
end
setAuxFunctions()
end
do
StringStream = {}
setmetatable(StringStream, {
__call = function(self)
local obj = {}
return setmetatable(obj, {__index = StringStream})
end
})
function StringStream.append(self, str, ...)
self[#self + 1] = string.format(str, ...)
end
function StringStream.concat(self, sep)
return table.concat(self, sep)
end
local function internalItemGetNameDescription(it, item, subType, addArticle)
subType = subType or (item and item:getSubType() or -1)
local ss = StringStream()
local obj = item or it
local name = obj:getName()
if name ~= "" then
if it:isStackable() and subType > 1 then
if it:hasShowCount() then
ss:append("%d ", subType)
end
ss:append("%s", obj:getPluralName())
else
if addArticle and obj:getArticle() ~= "" then
ss:append("%s ", obj:getArticle())
end
ss:append("%s", obj:getName())
end
else
ss:append("an item of type %d", obj:getId())
end
return ss:concat()
end
function Item:getNameDescription(subType, addArticle)
return internalItemGetNameDescription(self:getType(), self, subType, addArticle)
end
function ItemType:getNameDescription(subType, addArticle)
return internalItemGetNameDescription(self, nil, subType, addArticle)
end
end
do
-- Lua item descriptions created by Zbizu
local housePriceVisible = configManager.getBoolean(configKeys.HOUSE_DOOR_SHOW_PRICE)
local pricePerSQM = configManager.getNumber(configKeys.HOUSE_PRICE)
local showAtkWeaponTypes = {WEAPON_CLUB, WEAPON_SWORD, WEAPON_AXE, WEAPON_DISTANCE}
local showDefWeaponTypes = {WEAPON_CLUB, WEAPON_SWORD, WEAPON_AXE, WEAPON_DISTANCE, WEAPON_SHIELD}
local suppressedConditionNames = {
-- NOTE: these names are made up just to match dwarven ring attribute style
[CONDITION_POISON] = "antidote",
[CONDITION_FIRE] = "non-inflammable",
[CONDITION_ENERGY] = "antistatic",
[CONDITION_BLEEDING] = "antiseptic",
[CONDITION_HASTE] = "heavy",
[CONDITION_PARALYZE] = "slow immunity",
[CONDITION_DRUNK] = "hard drinking",
[CONDITION_DROWN] = "water breathing",
[CONDITION_FREEZING] = "warm",
[CONDITION_DAZZLED] = "dazzle immunity",
[CONDITION_CURSED] = "curse immunity"
}
-- first argument: Item, itemType or item id
local function internalItemGetDescription(item, lookDistance, subType, addArticle)
-- optional, but true by default
if addArticle == nil then
addArticle = true
end
local isVirtual = false -- check if we're inspecting a real item or itemType only
local itemType
-- determine the kind of item data to process
if tonumber(item) then
-- number in function argument
-- pull data from itemType instead
isVirtual = true
itemType = ItemType(item)
if not itemType then
return
end
-- polymorphism for item attributes (atk, def, etc)
item = itemType
elseif item:isItemType() then
isVirtual = true
itemType = item
else
itemType = item:getType()
end
-- use default values if not provided
lookDistance = lookDistance or -1
subType = subType or (not isVirtual and item:getSubType() or -1)
-- possibility of picking the item up (will be reused later)
local isPickupable = itemType:isMovable() and itemType:isPickupable()
-- has uniqueId tag
local isUnique = false
if not isVirtual then
local uid = item:getUniqueId()
if uid > 100 and uid < 0xFFFF then
isUnique = true
end
end
-- read item name with article
local itemName = item:getNameDescription(subType, addArticle)
-- things that will go in parenthesis "(...)"
local descriptions = {}
-- spell words
do
local spellName = itemType:getRuneSpellName()
if spellName then
descriptions[#descriptions + 1] = string.format('"%s"', spellName)
end
end
-- container capacity
do
-- show container capacity only on non-quest containers
if not isUnique then
local isContainer = itemType:isContainer()
if isContainer then
descriptions[#descriptions + 1] = string.format("Vol:%d", itemType:getCapacity())
end
end
end
-- actionId (will be reused)
local actionId = 0
if not isVirtual then
actionId = item:getActionId()
end
-- key
do
if not isVirtual and itemType:isKey() then
descriptions[#descriptions + 1] = string.format("Key:%0.4d", actionId)
end
end
-- weapon type (will be reused)
local weaponType = itemType:getWeaponType()
-- attack attributes
do
local attack = item:getAttack()
-- bows and crossbows
-- range, attack, hit%
if itemType:isBow() then
descriptions[#descriptions + 1] = string.format("Range:%d", item:getShootRange())
if attack ~= 0 then
descriptions[#descriptions + 1] = string.format("Atk+%d", attack)
end
local hitPercent = item:getHitChance()
if hitPercent ~= 0 then
descriptions[#descriptions + 1] = string.format("Hit%%+%d", hitPercent)
end
-- melee weapons and missiles
-- atk x physical +y% element
elseif table.contains(showAtkWeaponTypes, weaponType) then
local atkString = string.format("Atk:%d", attack)
local elementDmg = itemType:getElementDamage()
if elementDmg ~= 0 then
atkString = string.format("%s physical %+d %s", atkString, elementDmg, getCombatName(itemType:getElementType()))
end
descriptions[#descriptions + 1] = atkString
end
end
-- attack speed
do
local atkSpeed = item:getAttackSpeed()
if atkSpeed ~= 0 then
descriptions[#descriptions + 1] = string.format("AS:%0.2f/turn", 2000 / atkSpeed)
end
end
-- defense attributes
do
local showDef = table.contains(showDefWeaponTypes, weaponType)
local ammoType = itemType:getAmmoType()
if showDef then
local defense = item:getDefense()
local defAttrs = {}
if weaponType == WEAPON_DISTANCE then
-- throwables
if ammoType ~= AMMO_ARROW and ammoType ~= AMMO_BOLT then
defAttrs[#defAttrs + 1] = defense
end
else
defAttrs[#defAttrs + 1] = defense
end
-- extra def
local xD = item:getExtraDefense()
if xD ~= 0 then
defAttrs[#defAttrs + 1] = string.format("%+d", xD)
end
if #defAttrs > 0 then
descriptions[#descriptions + 1] = string.format("Def:%s", table.concat(defAttrs, " "))
end
end
end
-- armor
do
local arm = item:getArmor()
if arm > 0 then
descriptions[#descriptions + 1] = string.format("Arm:%d", arm)
end
end
-- abilities (will be reused)
local abilities = itemType:getAbilities()
-- stats: hp/mp/soul/magic level
do
local stats = {}
-- flat buffs
for stat, value in pairs(abilities.stats) do
stats[stat] = {name = getStatName(stat-1)}
if value ~= 0 then
stats[stat].flat = value
end
end
-- percent buffs
for stat, value in pairs(abilities.statsPercent) do
if value ~= 0 then
stats[stat].percent = value
end
end
-- display the buffs
for _, statData in pairs(stats) do
local displayValues = {}
if statData.flat then
displayValues[#displayValues + 1] = statData.flat
end
if statData.percent then
displayValues[#displayValues + 1] = statData.percent
end
-- desired format examples:
-- +5%
-- +20 and 5%
if #displayValues > 0 then
displayValues[1] = string.format("%+d", displayValues[1])
descriptions[#descriptions + 1] = string.format("%s %s", statData.name, table.concat(displayValues, " and "))
end
end
end
-- skill boosts
do
for skill, value in pairs(abilities.skills) do
if value ~= 0 then
descriptions[#descriptions + 1] = string.format("%s %+d", getSkillName(skill-1), value)
end
end
end
-- element magic level
do
for element, value in pairs(abilities.specialMagicLevel) do
if value ~= 0 then
descriptions[#descriptions + 1] = string.format("%s magic level %+d", getCombatName(2^(element-1)), value)
end
end
end
-- special skills
do
for skill, value in pairs(abilities.specialSkills) do
if value ~= 0 then
-- add + symbol to special skill "amount" fields
if skill-1 < 6 and skill % 2 == 0 and not (skill-1 == SPECIALSKILL_CRITICALHITAMOUNT) then
value = string.format("%+d", value/100)
elseif skill-1 < 6 and skill % 2 == 1 then
value = string.format("%+d", value)
elseif skill-1 >= 6 then
-- fatal, dodge, momentum coming from the item natively
-- (stats coming from tier are near tier info)
value = string.format("%0.2f", value/100)
end
descriptions[#descriptions + 1] = string.format("%s %s%%", getSpecialSkillName(skill-1), value)
end
end
end
-- cleave
-- perfect shot
-- to do
-- protections
do
local protections = {}
for element, value in pairs(abilities.absorbPercent) do
if value ~= 0 then
protections[#protections + 1] = string.format("%s %+d%%", getCombatName(2^(element-1)), value)
end
end
if #protections > 0 then
descriptions[#descriptions + 1] = string.format("protection %s", table.concat(protections, ", "))
end
end
-- damage reflection
-- to do
-- magic shield (classic)
if abilities.manaShield then
descriptions[#descriptions + 1] = "magic shield"
end
-- magic shield capacity +flat and x%
-- to do
-- regeneration
if abilities.manaGain > 0 or abilities.healthGain > 0 or abilities.regeneration then
descriptions[#descriptions + 1] = "faster regeneration"
end
-- invisibility
if abilities.invisible then
descriptions[#descriptions + 1] = "invisibility"
end
-- condition immunities
do
local suppressions = abilities.conditionSuppressions
for conditionId, conditionName in pairs(suppressedConditionNames) do
if bit.band(abilities.conditionSuppressions, conditionId) ~= 0 then
descriptions[#descriptions + 1] = conditionName
end
end
end
-- speed
if abilities.speed ~= 0 then
descriptions[#descriptions + 1] = string.format("speed %+d", math.floor(abilities.speed / 2))
end
-- collecting attributes finished
-- build the output text
local response = {itemName}
-- item group (will be reused)
local itemGroup = itemType:getGroup()
-- fluid type
do
if (itemGroup == ITEM_GROUP_FLUID or itemGroup == ITEM_GROUP_SPLASH) and subType > 0 then
local subTypeName = getSubTypeName(subType)
response[#response + 1] = string.format(" of %s", (subTypeName ~= '' and subTypeName or "unknown"))
end
end
-- door check (will be reused)
local isDoor = itemType:isDoor()
-- door info
if isDoor then
if not isVirtual then
local minLevelDoor = itemType:getLevelDoor()
if minLevelDoor ~= 0 then
if actionId >= minLevelDoor then
response[#response + 1] = string.format(" for level %d", actionId - minLevelDoor)
end
end
end
end
-- primary attributes parenthesis
if #descriptions > 0 then
response[#response + 1] = string.format(" (%s)", table.concat(descriptions, ", "))
end
-- podium description
if not isVirtual and itemType:isPodium() then
local outfit = item:getOutfit()
local hasOutfit = item:hasFlag(PODIUM_SHOW_OUTFIT)
local hasMount = item:hasFlag(PODIUM_SHOW_MOUNT)
if outfit then
local podiumParts = {}
local outfitType = Outfit(outfit.lookType)
if outfitType then
if hasOutfit then
podiumParts[#podiumParts + 1] = string.format("%s outfit", outfitType.name)
end
-- search mount
local mountName = getMountNameByLookType(outfit.lookMount)
if mountName and hasMount then
podiumParts[#podiumParts + 1] = string.format("%s mount", mountName)
end
else
-- search mount
local mountName = getMountNameByLookType(outfit.lookMount)
local isEnabled = mountName and hasMount
if not mountName then
mountName = getMountNameByLookType(outfit.lookType)
isEnabled = mountName and hasOutfit
end
if mountName and isEnabled then
podiumParts[#podiumParts + 1] = string.format("%s mount", mountName)
end
end
if #podiumParts > 0 then
response[#response + 1] = string.format(" displaying the %s", table.concat(podiumParts, " on the "))
end
end
end
-- charges and duration
do
local expireInfo = {}
-- charges
if itemType:hasShowCharges() then
local charges = item:getCharges()
expireInfo[#expireInfo + 1] = string.format("has %d charge%s left", charges, (charges ~= 1 and "s" or ""))
end
-- duration
if itemType:hasShowDuration() then
local currentDuration = item:getDuration()
if isVirtual then
currentDuration = currentDuration * 1000
end
local maxDuration = itemType:getDuration() * 1000
if maxDuration == 0 then
local transferType = itemType:getTransformEquipId()
if transferType ~= 0 then
transferType = ItemType(transferType)
maxDuration = transferType and transferType:getDuration() * 1000 or maxDuration
end
end
if currentDuration == maxDuration then
expireInfo[#expireInfo + 1] = "is brand-new"
elseif currentDuration ~= 0 then
expireInfo[#expireInfo + 1] = string.format("will expire in %s", Game.getCountdownString(math.floor(currentDuration/1000), true, true))
end
end
if #expireInfo > 0 then
response[#response + 1] = string.format(" that %s", table.concat(expireInfo, " and "))
end
end
-- dot after primary attributes info
response[#response + 1] = "."
-- empty fluid container suffix
if itemGroup == ITEM_GROUP_FLUID and subType < 1 then
response[#response + 1] = " It is empty."
end
-- house door
if isDoor and not isVirtual then
local tile = item:getTile()
if tile then
local house = tile:getHouse()
if house then
local houseName = house:getName()
local houseOwnerName = house:getOwnerName()
local isForSale = false
if not houseOwnerName or houseOwnerName:len() == 0 then
houseOwnerName = "Nobody"
isForSale = true
end
response[#response + 1] = string.format(" It belongs to house '%s'. %s owns this house.", houseName, houseOwnerName)
if housePriceVisible and isForSale and pricePerSQM > 0 then
response[#response + 1] = string.format(" It costs %d gold coins.", pricePerSQM * house:getTileCount())
end
end
end
end
--[[
-- imbuements (to do)
-- \nImbuements: (Basic Strike 2:30h, Basic Void 2:30h, Empty Slot).
-- item class (placeholder)
-- Classification: x.
-- Disabled until classification system is implemented.
do
local classification = itemType:getClassification()
if classification == 0 then
classification = "other"
end
response[#response + 1] = string.format("\nClassification: %s.", classification)
end
]]
-- item count (will be reused later)
local count = isVirtual and 1 or item:getCount()
-- wield info
do
if itemType:isRune() then
local rune = Spell(itemType:getId())
if rune then
local runeLevel = rune:runeLevel()
local runeMagLevel = rune:runeMagicLevel()
local runeVocMap = rune:vocation()
local hasLevel = runeLevel > 0
local hasMLvl = runeMagLevel > 0
local hasVoc = #runeVocMap > 0
if hasLevel or hasMLvl or hasVoc then
local vocAttrs = {}
if not hasVoc then
vocAttrs[#vocAttrs + 1] = "players"
else
for _, vocName in ipairs(runeVocMap) do
local vocation = Vocation(vocName)
if vocation and vocation:getPromotion() then
vocAttrs[#vocAttrs + 1] = vocName
end
end
end
if #vocAttrs > 1 then
vocAttrs[#vocAttrs - 1] = string.format("and %s", vocAttrs[#vocAttrs - 1])
end
local levelInfo = {}
if hasLevel then
levelInfo[#levelInfo + 1] = string.format("level %d", runeLevel)
end
if hasMLvl then
levelInfo[#levelInfo + 1] = string.format("magic level %d", runeMagLevel)
end
local levelStr = ""
if #levelInfo > 0 then
levelStr = string.format(" of %s or higher", table.concat(levelInfo, " and "))
end
response[#response + 1] = string.format(
"\n%s can only be used properly by %s%s.",
(count > 1 and "They" or "It"),
table.concat(vocAttrs, ", "),
levelStr
)
end
end
else
local wieldInfo = itemType:getWieldInfo()
if wieldInfo ~= 0 then
local wieldAttrs = {}
if bit.band(wieldInfo, WIELDINFO_PREMIUM) ~= 0 then
wieldAttrs[#wieldAttrs + 1] = "premium"
end
local vocStr = itemType:getVocationString()
if vocStr ~= '' then
wieldAttrs[#wieldAttrs + 1] = vocStr
else
wieldAttrs[#wieldAttrs + 1] = "players"
end
local levelInfo = {}
if bit.band(wieldInfo, WIELDINFO_LEVEL) ~= 0 then
levelInfo[#levelInfo + 1] = string.format("level %d", itemType:getMinReqLevel())
end
if bit.band(wieldInfo, WIELDINFO_MAGLV) ~= 0 then
levelInfo[#levelInfo + 1] = string.format("magic level %d", itemType:getMinReqMagicLevel())
end
if #levelInfo > 0 then
wieldAttrs[#wieldAttrs + 1] = string.format("of %s or higher", table.concat(levelInfo, " and "))
end
response[#response + 1] = string.format(
"\n%s can only be wielded properly by %s.",
(count > 1 and "They" or "It"),
table.concat(wieldAttrs, " ")
)
end
end
end
if lookDistance <= 1 then
local weight = item:getWeight()
if isPickupable and not isUnique then
response[#response + 1] = string.format("\n%s %0.2f oz.", (count == 1 or not itemType:hasShowCount()) and "It weighs" or "They weigh", weight / 100)
end
end
-- item text
if not isVirtual and itemType:hasAllowDistRead() then
local text = item:getText()
if text and text:len() > 0 then
if lookDistance <= 4 then
local writer = item:getAttribute(ITEM_ATTRIBUTE_WRITER)
local writeDate = item:getAttribute(ITEM_ATTRIBUTE_DATE)
local writeInfo = {}
if writer and writer:len() > 0 then
writeInfo[#writeInfo + 1] = string.format("\n%s wrote", writer)
if writeDate and writeDate > 0 then
writeInfo[#writeInfo + 1] = string.format(" on %s", os.date("%d %b %Y", writeDate))
end
end
if #writeInfo > 0 then
response[#response + 1] = string.format("%s:\n%s", table.concat(writeInfo, ""), text)
else
response[#response + 1] = string.format("\nYou read: %s", text)
end
else
response[#response + 1] = "\nYou are too far away to read it."
end
else
response[#response + 1] = "\nNothing is written on it."
end
end
-- item description
local isBed = itemType:isBed()
if lookDistance <= 1 or (not isVirtual and (isDoor or isBed)) then
-- custom item description
local desc = not isVirtual and item:getSpecialDescription()
-- native item description
if not desc or desc == "" then
desc = itemType:getDescription()
end
-- level door description
if isDoor and lookDistance <= 1 and (not desc or desc == "") and itemType:getLevelDoor() ~= 0 then
desc = "Only the worthy may pass."
end
if desc and desc:len() > 0 then
response[#response + 1] = string.format("\n%s", desc)
end
else
if lookDistance <= 4 then
local desc = not isVirtual and item:getSpecialDescription()
if not desc or desc == "" then
desc = itemType:getDescription()
end
if desc and desc:len() > 0 then
response[#response + 1] = string.format("\n%s", desc)
end
else
response[#response + 1] = "\nYou are too far away to read it."
end
end
-- pickupable items with store flag
if not isVirtual and isPickupable and item:isStoreItem() then
response[#response + 1] = "\nThis item cannot be traded."
end
-- turn response into a single string
return table.concat(response, "")
end
function Item:getDescription(lookDistance, subType, addArticle)
return internalItemGetDescription(self, lookDistance, subType, addArticle)
end
function ItemType:getItemDescription(lookDistance, subType, addArticle)
return internalItemGetDescription(self, lookDistance, subType, addArticle)
end
end