Module:Sandbox/AuronTest: Difference between revisions

Updated everything for 0.21 (we hope)
(0.21 Special attack description handling)
(Updated everything for 0.21 (we hope))
Line 1: Line 1:
-- Used for testing Lua related bits without the possibility of impacting modules that are actively generating page content
-- Used for testing Lua related bits without the possibility of impacting modules that are actively generating page content
-- TODO:
--  Special attack description processing
--  Anything else combat related


--This module contains all sorts of functions for getting data on items
--This module contains all sorts of functions for getting data on items
Line 134: Line 131:
   if Shared.contains(ItemData.EquipmentStatKeys, StatName) then
   if Shared.contains(ItemData.EquipmentStatKeys, StatName) then
     local equipStats = p._processEquipmentStats(item.equipmentStats)
     local equipStats = p._processEquipmentStats(item.equipmentStats)
     if equipStats[StatName] == nil then
     result = equipStats[StatName]
      result = nil
    else
      result = equipStats[StatName]
    end
   elseif StatName == 'attackType' then
   elseif StatName == 'attackType' then
     result = p._getWeaponAttackType(item)
     result = p._getWeaponAttackType(item)
Line 209: Line 202:


function p.hasCombatStats(item)
function p.hasCombatStats(item)
   if item.equipmentSlot == nil then
   if not item.isEquipment or item.validSlots == nil and item.equipmentStats ~= nil then
     return false
     -- Ensure at least one stat has a non-zero value
  elseif item.equipmentSlot == Constants.getEquipmentSlotID('Weapon') then
    local equipStats = p._processEquipmentStats(item.equipmentStats)
     return true
     for statName, statVal in pairs(equipStats) do
  end
      if statVal ~= 0 then return true end
 
  local combatStatList = {'strengthBonus', 'defenceBonus', 'stabAttackBonus', 'slashAttackBonus', 'blockAttackBonus', 'damageReduction',
                          'rangedDefenceBonus', 'magicDefenceBonus', 'rangedAttackBonus', 'rangedStrengthBonus', 'magicAttackBonus', 'magicDamageBonus'}
 
  for i, stat in Shared.skpairs(combatStatList) do
    if p._getItemStat(item, stat, true) > 0 then
      return true
     end
     end
   end
   end
   return false
   return false
end
end
Line 249: Line 234:


function p._getWeaponAttackType(item)
function p._getWeaponAttackType(item)
   if item.type == 'Weapon' then
   if item.isEquipment == true and item.validSlots ~= nil and Shared.contains(item.validSlots, 'Weapon') then
    return Icons.Icon({'Melee', nolink='true'})
     if Shared.contains({'melee', 'ranged', 'magic'}, item.attackType) then
  elseif item.type == 'Ranged Weapon' then
      local iconType = item.attackType ~= 'melee' and 'skill' or nil
     return Icons.Icon({'Ranged', type='skill', nolink='true'})
      return Icons.Icon({Shared.titleCase(item.attackType), type=iconType, nolink='true'})
  elseif item.type == 'Magic Staff' or item.type == 'Magic Wand' then
     end
    return Icons.Icon({'Magic', type='skill', nolink='true'})
  else
     return "Invalid"
   end
   end
  return 'Invalid'
end
end


Line 303: Line 286:
   --For weapons with a special attack, show the details
   --For weapons with a special attack, show the details
   if item.hasSpecialAttack then
   if item.hasSpecialAttack then
     local spAtt = p.getSpecialAttackByID(item.specialAttackID)
     local spAttPart = {}
     result = result.."\r\n|-\r\n|'''Special Attack:'''"
     table.insert(spAttPart, "\r\n|-\r\n|'''Special Attack:'''")
     result = result..'\r\n* '..spAtt.chance..'% chance for '..spAtt.name..':'
     for i, spID in ipairs(item.specialAttacks) do
    result = result..'\r\n** '..p._getSpecialAttackDescription(spAtt)
      local spAtt = p.getSpecialAttackByID(spID)
      table.insert(spAttPart, '\r\n* ' .. spAtt.defaultChance .. '% chance for ' .. spAtt.name .. ':')
      table.insert(spAttPart, '\r\n** ' .. spAtt.description)
    end
    result = result .. table.concat(spAttPart)
   end
   end
   --For potions, show the number of charges
   --For potions, show the number of charges
Line 347: Line 334:
   if item.tier ~= nil then result = result..'[[Category:'..Shared.titleCase(item.tier)..' '..item.type..']]' end
   if item.tier ~= nil then result = result..'[[Category:'..Shared.titleCase(item.tier)..' '..item.type..']]' end
   if item.hasSpecialAttack then result = result..'[[Category:Items With Special Attacks]]' end
   if item.hasSpecialAttack then result = result..'[[Category:Items With Special Attacks]]' end
   if item.isPassiveItem then result = result..'[[Category:Passive Items]]' end
   if item.validSlots ~= nil and Shared.contains(item.validSlots, 'Passive') then result = result..'[[Category:Passive Items]]' end
   if item.chanceToDoubleLoot ~= nil and item.chanceToDoubleLoot > 0 then result = result..'[[Category:Double Loot Chance Items]]' end
   if item.modifiers ~= nil then
    local modsDL = {
      'increasedChanceToDoubleLootCombat',
      'decreasedChanceToDoubleLootCombat',
      'increasedChanceToDoubleLootThieving',
      'decreasedChanceToDoubleLootThieving'
    }
    for modName, val in pairs(item.modifiers) do
      if Shared.contains(modsDL, modName) then
        result = result..'[[Category:Double Loot Chance Items]]'
        break
      end
    end
  end
   return result
   return result
end
end
Line 392: Line 392:
   for i, item in Shared.skpairs(ItemData.Items) do
   for i, item in Shared.skpairs(ItemData.Items) do
     if item.hasSpecialAttack then
     if item.hasSpecialAttack then
       if spAttTable[item.specialAttackID] == nil then spAttTable[item.specialAttackID] = {sortName=item.name, Icons = {}} end
       for i, spID in ipairs(item.specialAttacks) do
      table.insert(spAttTable[item.specialAttackID].Icons, Icons.Icon({item.name, type='item'}))
        local spAtt = p.getSpecialAttackByID(spID)
        if spAttTable[spID] == nil then spAttTable[spID] = {sortName=item.name, Icons = {}} end
        table.insert(spAttTable[spID].Icons, Icons.Icon({item.name, type='item'}))
      end
     end
     end
   end   
   end   
Line 405: Line 408:
     result = result..'\r\n|-'
     result = result..'\r\n|-'
     result = result..'\r\n|data-sort-value="'..spAttData.sortName..'"|'..table.concat(spAttData.Icons, '<br/>')
     result = result..'\r\n|data-sort-value="'..spAttData.sortName..'"|'..table.concat(spAttData.Icons, '<br/>')
     result = result..'||'..spAtt.name..'||data-sort-value="'..spAtt.chance..'"|'..spAtt.chance..'%'
     result = result..'||'..spAtt.name..'||data-sort-value="'..spAtt.defaultChance..'"|'..spAtt.defaultChance..'%'
     result = result..'||'..p._getSpecialAttackDescription(spAtt)
     result = result..'||'..spAtt.description
   end
   end
   result = result..'\r\n|}'
   result = result..'\r\n|}'
Line 412: Line 415:
   return result
   return result
end
end
local nouns = {
  ["player"] = {
    ["plain"] = "you",
    ["possesive"] = "your",
    ["pronoun"] = "you",
    ["is"] = "are"
  },
  ["enemy"] = {
    ["plain"] = "the enemy",
    ["possesive"] = "the enemy's",
    ["pronoun"] = "they",
    ["is"] = "is"
  }
}
-- Generates a textual description from a special attack's damage attribute
-- Similar to in game function getDamageDescription in attack.js
function p._getDamageDescription(damageList, attName, tarName)
  local rollData = {
    ["CurrentHP"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '%' end,
      ["formatName"] = function(name) return ' of ' .. name .. ' current hitpoints' end
    },
    ["MaxHP"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '%' end,
      ["formatName"] = function(name) return ' of ' .. name .. ' max hitpoints' end
    },
    ["DamageDealt"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '%' end,
      ["formatName"] = function(name) return ' of the damage dealt' end
    },
    ["MaxHit"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '%' end,
      ["formatName"] = function(name) return ' of ' .. name .. ' max hit' end
    },
    ["MinHit"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '%' end,
      ["formatName"] = function(name) return ' of ' .. name .. ' min hit' end
    },
    ["Fixed"] = {
      ["formatPercent"] = function(value) return Shared.formatnum(math.floor(value * 10)) end,
      ["formatName"] = function(name) return '' end
    },
    ["MagicScaling"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 0, 0) .. '%' end,
      ["formatName"] = function(name) return ' of ' .. name .. ' current hitpoints' end
    },
    ["One"] = {
      ["formatPercent"] = function(value) return '1' end,
      ["formatName"] = function(name) return '' end
    },
    ["Rend"] = {
      ["formatPercent"] = function(value) return Shared.round(value, 2, 0) .. '% if the target has full HP, otherwise 250%' end,
      ["formatName"] = function(name) return ' of the damage dealt' end
    },
  }
  local descPart = {}
  for i, damage in ipairs(damageList) do
    local name = 'Unknown'
    if damage.character == 'Attacker' then
      name = nouns[attName]['possesive']
    elseif damage.character == 'Target' then
      name = nouns[tarName]['possesive']
    end
    local maxData = rollData[damage['maxRoll']]
    local desc = ''
    if damage.roll then
      local minData = rollData[damage['minRoll']]
      if damage['maxRoll'] == damage['minRoll'] then
        -- Same damage type for max & min
        desc = minData.formatPercent(damage['minPercent']) .. '-' .. maxData.formatPercent(damage['maxPercent']) .. minData.formatName(name)
      elseif damage['maxRoll'] == 'MaxHit' and damage['minRoll'] == 'MinHit' and damage['maxPercent'] == damage['minPercent'] then
        desc = maxData.formatPercent(damage['maxPercent']) .. ' of ' .. name .. ' normal damage'
      else
        desc = minData.formatPercent(damage['minPercent']) .. minData.formatName(name) .. ' to ' .. maxData.formatPercent(damage['maxPercent']) .. maxData.formatName(name)
      end
    else
      desc = maxData.formatPercent(damage['maxPercent']) .. maxData.formatName(name)
    end
    table.insert(descPart, desc)
  end
  return Shared.joinList(descPart, ', ', ' and ')
end
function p._getEffectDescription(effectList, attName, tarName)
  local getAllModText = function(modifierList, doColor)
                          local returnPart = {}
                          for modName, val in pairs(modifierList) do
                            table.insert(returnPart, Constants._getModifierText(modName, val, doColor))
                          end
                          return Shared.joinList(returnPart, ', ', ' and ')
                        end
  local descPart = {}
  for i, effect in ipairs(effectList) do
    local desc = ''
    if effect['type'] == 'DOT' then
      local damageDesc = p._getDamageDescription(effect['damage'], attName, tarName)
      local parts = nil
      if effect['subtype'] == 'Regen' then
        parts = {'give', 'heals', ''}
      else
        parts = {'inflict', 'deals', 'as damage '}
      end
      if effect['chance'] < 100 then
          desc = 'has a ' .. Shared.round(effect['chance'], 2, 0) .. '% chance to ' .. parts[1] .. ' ' .. effect['subtype'] .. ' '
      else
          desc = parts[1] .. 's ' .. effect['subtype'] .. ' '
      end
      desc = desc .. 'that ' .. parts[2] .. ' ' .. damageDesc .. ' ' .. parts[3] .. 'over ' .. Shared.round(effect['procs'] * effect['interval'] / 1000, 2, 0) .. 's'
    elseif effect['type'] == 'Reflexive' then
      local modDesc = getAllModText(effect.modifiers, false)
      desc = 'gives ' .. nouns[attName]['plain'] .. ' ' .. modDesc .. ' each time ' .. nouns[attName]['pronoun'] .. ' are hit for the duration of this attack (Stacks up to ' .. Shared.formatnum(math.floor(effect['maxStacks'])) .. ' times)'
    elseif effect['type'] == 'Stacking' then
      local modDesc = getAllModText(effect.modifiers, false)
      desc = 'applies +' .. Shared.formatnum(math.floor(effect['stacksToAdd'])) .. ' stack' .. (effect['stacksToAdd'] > 1 and 's' or '') .. ' of ' .. effect['name'] .. ' to ' .. nouns[tarName]['plain']
      desc = desc .. ' (Max ' .. Shared.formatnum(math.floor(effect['maxStacks'])) .. ' stack' .. (effect['maxStacks'] > 1 and 's' or '') .. '). ' .. effect['name'] .. ' gives ' .. modDesc .. ' regardless of number of stacks. One stack is removed after each of the ' .. nouns[tarName]['possesive'] .. ' turns'
    elseif effect['type'] == 'Modifier' then
      local modDesc = getAllModText(effect.modifiers, false)
      local name = effect['character'] == 'Attacker' and attName or tarName
      local countName = effect['countsOn'] == 'Attacker' and attName or tarName
      desc = 'gives ' .. nouns[name]['plain'] .. ' ' .. modDesc
      if effect['maxStacks'] > 1 then
        desc = desc .. ' that stacks up to ' .. Shared.formatnum(math.floor(effect['maxStacks'])) .. ' times'
      elseif effect['turns'] == nil then
        -- Take a nil turns value to mean Infinity, as in JS JSON.Stringify() converts Infinity to null
        -- This assumption will potentially cause issues in the future
        desc = desc .. ' until the end of the fight'
      elseif effect['turns'] > 0 then
        desc = desc .. ' for ' .. Shared.formatnum(math.floor(effect['turns'])) .. ' of ' .. nouns[countName]['possesive'] .. ' turn' .. (effect['turns'] > 1 and 's' or '')
      else
        desc = desc .. ' until the end of the attack'
      end
    elseif effect['type'] == 'Sleep' or effect['type'] == 'Stun' then
      local effName = (effect['type'] == 'Sleep' and 'sleep' or string.gsub(effect['flavour'], '^(%a)', function(c) return string.lower(c) end))
      if effect['chance'] < 100 then
        desc = 'has a ' .. Shared.round(effect['chance'], 2, 0) .. '%' .. ' chance to apply ' .. effName
      else
        desc = 'applies ' .. effName
      end
      desc = desc .. ' for ' .. Shared.formatnum(math.floor(effect['turns'])) .. ' turn' .. (effect['turns'] > 1 and 's' or '')
    else
      desc = 'Unknown effect type: ' .. effect['type'] .. '[[Category:Pages with script errors]]'
    end
    table.insert(descPart, desc)
  end
  return Shared.joinList(descPart, '; ', ' and ')
end
-- Generates a textual description for a special attack
-- Similar to in game function describeAttack in attack.js
function p._getSpecialAttackDescription(attack, targetIsEnemy)
  local attackDescriptors = {
    ["count"] = function(attack, attName, tarName) return attack['attackCount'] end,
    ["damage"] = function(attack, attName, tarName) return p._getDamageDescription(attack['damage'], attName, tarName) end,
    ["lifesteal"] = function(attack, attName, tarName) return Shared.round(attack['lifesteal'], 2, 0) .. '%' end,
    ["target"] = function(attack, attName, tarName) return nouns[tarName]['plain'] end,
    ["attacker"] = function(attack, attName, tarName) return nouns[attName]['plain'] end,
    ["tarPos"] = function(attack, attName, tarName) return nouns[tarName]['possesive'] end,
    ["attPos"] = function(attack, attName, tarName) return nouns[attName]['possesive'] end,
    ["prehitEffect"] = function(attack, attName, tarName) return p._getEffectDescription(attack['prehitEffects'], attName, tarName) end,
    ["hitEffect"] = function(attack, attName, tarName) return p._getEffectDescription(attack['onhitEffects'], attName, tarName) end,
    ["canMiss"] = function(attack, attName, tarName) return attack['cantMiss'] and "can't miss" or "can miss" end,
    ["avoidable"] = function(attack, attName, tarName) return attack['cantMiss'] and "unavoidable" or "avoidable" end,
    ["duration"] = function(attack, attName, tarName) return Shared.round((attack['attackCount'] - 1) * attack['attackInterval'] / 1000, 2, 0) .. 's' end,
    ["interval"] = function(attack, attName, tarName) return Shared.round(attack['attackInterval'] / 1000, 2, 0) .. 's' end,
    ["tarIs"] = function(attack, attName, tarName) return nouns[tarName]['is'] end,
    ["attIs"] = function(attack, attName, tarName) return nouns[attName]['is'] end,
  }
  -- string.gsub() searches for a pattern rather than a literal and is case sensitive, so use the below to:
  -- 1. Escape any characters that have special meanings within patterns
  -- 2. Modify the pattern to be case insensitive (by replacing each alpha char with a class containing
  --    both the uppercase & lowercase characters)
  local patternFormat = function(str, caseInsensitive)
                          local pat = string.gsub(str, '[%(%)%.%%+%-%*%?%[%]%^%$]', function(c) return '%' .. c end)
                          if caseInsensitive then pat = string.gsub(pat, '(%a)', function(c) return '[' .. string.upper(c) .. string.lower(c) .. ']' end) end
                          return pat
                        end
  local attackDesc = attack['description']
  local attName, tarName = targetIsEnemy and 'player' or 'enemy', targetIsEnemy and 'enemy' or 'player'
  for desc, repFunc in pairs(attackDescriptors) do
    local searchPat = '<' .. patternFormat(desc, true) .. '>'
    if string.find(attackDesc, searchPat) ~= nil then
      local repText = repFunc(attack, attName, tarName)
      repText = patternFormat(string.upper(string.sub(repText, 1, 1)) .. string.sub(repText, 2))
      attackDesc = string.gsub(attackDesc, searchPat, repText)
    end
  end
  return attackDesc
end
-- mw.log(p._getSpecialAttackDescription(p.getSpecialAttackByID(84), false))
-- mw.log(p._getSpecialAttackDescription(p.getSpecialAttackByID(31), true))


return p
return p