Difference between revisions of "Module:Monster"

From CrawlWiki
Jump to: navigation, search
(Death knight needs disambiguation)
m (Energy Bolt -> Bolt of Devastation)
 
(17 intermediate revisions by 5 users not shown)
Line 19: Line 19:
 
   end
 
   end
 
   return name
 
   return name
 +
end
 +
 +
local function tile_name_arg(frame)
 +
  local tile_name = frame.args[2]
 +
  local monster_name = frame.args[1]
 +
  if not tile_name or tile_name == "" then
 +
    tile_name = monster_name
 +
  end
 +
  if not tile_name or tile_name == "" then
 +
    tile_name = mw.title.getCurrentTitle().text
 +
  end
 +
  return tile_name
 
end
 
end
  
Line 40: Line 52:
 
   local args = {}
 
   local args = {}
 
   args.name = monster.Name
 
   args.name = monster.Name
 +
  args.tile_name = tile_name_arg(frame)
 
   args.glyph = frame:expandTemplate{title = monster.Colour, args = {monster.Glyph}}
 
   args.glyph = frame:expandTemplate{title = monster.Colour, args = {monster.Glyph}}
  
Line 78: Line 91:
 
   args.magic_resistance = monster.MR
 
   args.magic_resistance = monster.MR
  
   local hp_min = monster.HD*monster["Base HP"] + monster["Fixed HP"]
+
   local avg_hp = monster["Average HP 10x"] / 10
   local hp_max = monster.HD*(monster["Base HP"]+monster["Rand HP"])+monster["Fixed HP"]
+
   local hp_min = math.max(math.floor(0.66 * avg_hp), 1)
   if hp_min == hp_max then
+
   local hp_max = math.ceil(1.33 * avg_hp)
    args.hp_range = hp_min
+
   args.hp_range = ("%d-%d"):format(hp_min, hp_max)
   else
+
   args.avg_hp = avg_hp
    args.hp_range = ("%d-%d"):format(hp_min, hp_max)
 
  end
 
   args.avg_hp = (hp_min + hp_max)/2
 
  
 
   args.armour_class = monster.AC
 
   args.armour_class = monster.AC
Line 126: Line 136:
 
   flavour = frame:expandTemplate{title = "flavour", args = {flavour}}
 
   flavour = frame:expandTemplate{title = "flavour", args = {flavour}}
 
   return infobox .. "\n" .. flavour
 
   return infobox .. "\n" .. flavour
 +
end
 +
 +
local spell_disambig = {
 +
  ["Phantom Mirror"] = "Phantom Mirror (spell)",
 +
}
 +
 +
local function spell_link(name)
 +
    if spell_disambig[name] then
 +
        return '[[' .. spell_disambig[name] .. '|' .. name .. ']]'
 +
    else
 +
        return '[[' .. name .. ']]'
 +
    end
 
end
 
end
  
Line 138: Line 160:
 
   for i, spells in ipairs(monster.Spellsets) do
 
   for i, spells in ipairs(monster.Spellsets) do
 
     ret = ret .. [=[<div style="margin: 1.0em; margin-top:0; margin-right:0; padding: 0px; float: left; border:none;">
 
     ret = ret .. [=[<div style="margin: 1.0em; margin-top:0; margin-right:0; padding: 0px; float: left; border:none;">
{| class="prettytable" style="border:none; margin:0; padding:0; width:16em;"
+
{| class="prettytable" style="border:none; margin:0; padding:0; width:28em;"
 
! colspan="3" style="font-size:larger;" | Spell set ]=] .. roman[i] .. "\n"
 
! colspan="3" style="font-size:larger;" | Spell set ]=] .. roman[i] .. "\n"
 
     for j, spell in ipairs(spells) do
 
     for j, spell in ipairs(spells) do
       ret = ret .. '|-\n! align="left" | Slot<sup>' .. j .. '</sup>\n| [[' .. spell.Spell .. ']]'
+
       ret = ret .. '|-\n! align="left" | Slot<sup>' .. j .. '</sup>\n| ' .. spell_link(spell.Spell)
 
       if spell.Damage then
 
       if spell.Damage then
 
         ret = ret .. " (" .. spell.Damage .. ")"
 
         ret = ret .. " (" .. spell.Damage .. ")"
Line 150: Line 172:
 
         flags[i] = frame:expandTemplate{title = v .. " slot flag"}
 
         flags[i] = frame:expandTemplate{title = v .. " slot flag"}
 
       end
 
       end
       ret = ret .. table.concat(flags, ", ") .. "\n"
+
       ret = ret .. table.concat(flags, ",<br> ") .. "\n"
 
     end
 
     end
 
     ret = ret .. "|-\n|}</div>"
 
     ret = ret .. "|-\n|}</div>"
 
     cleared = i % 2 == 0
 
     cleared = i % 2 == 0
 
     if cleared then
 
     if cleared then
       ret = ret .. '<div style="clear:both;"></div>'
+
       ret = ret .. '<div style="clear:left;"></div>'
 
     end
 
     end
 
   end
 
   end
 
   if not cleared then
 
   if not cleared then
     ret = ret .. '<div style="clear:both;"></div>'
+
     ret = ret .. '<div style="clear:left;"></div>'
 
   end
 
   end
 
   return ret
 
   return ret
 
end
 
end
  
local lich_spells = {
+
local panlord_spells = {
   ["Corrosive Bolt"] = 101,
+
-- 3.4% chance to have a particular one of these spells
   ["Lehudib's Crystal Spear"] = 101,
+
-- (3/4 chance to be a spellcaster, then 9/10 chance to have either summon or aoe,
   ["Orb of Destruction"] = 101,
+
--  then 1/2 chance to choose an aoe spell, then there are 10 possible spells)
   ["Summon Greater Demon"] = 50,
+
  ["Symbol of Torment"] = 3.4,
   ["Banishment"] = 33.3,
+
  ["Fire Storm"] = 3.4,
   ["Haste"] = 33.3,
+
  ["Glaciate"] = 3.4,
   ["Invisibility"] = 33.3,
+
  ["Chain Lightning"] = 3.4,
   ["Agony"] = 101,
+
  ["Freezing Cloud"] = 3.4,
   ["Bolt of Cold"] = 101,
+
  ["Poisonous Cloud"] = 3.4,
   ["Bolt of Draining"] = 101,
+
  ["Metal Splinters"] = 3.4,
   ["Bolt of Fire"] = 101,
+
   ["Bolt of Devastation"] = 3.4,
   ["Confuse"] = 101,
+
  ["Orb of Electricity"] = 3.4,
   ["Fireball"] = 101,
+
  ["Conjure Ball Lightning"] = 3.4,
   ["Haunt"] = 101,
+
 
   ["Iron Shot"] = 101,
+
-- 5.2% chance to have a particular one of these spells
   ["Iskenderun's Mystic Blast"] = 101,
+
-- (3/4 chance to be a spellcaster, then 9/10 chance to choose one of them, then there are 13 possible spells)
   ["Iskenderun's Battlesphere"] = 101,
+
  ["Call Down Damnation"] = 5.2,
   ["Lee's Rapid Deconstruction"] = 101,
+
   ["Lehudib's Crystal Spear"] = 5.2,
   ["Lightning Bolt"] = 101,
+
  ["Corrosive Bolt"] = 5.2,
   ["Malign Gateway"] = 101,
+
  ["Quicksilver Bolt"] = 5.2,
   ["Paralyse"] = 101,
+
   ["Orb of Destruction"] = 5.2,
   ["Petrify"] = 101,
+
   ["Energy Bolt"] = 5.2,
   ["Poison Arrow"] = 101,
+
   ["Mindburst"] = 5.2,
   ["Spellforged Servitor"] = 101,
+
   ["Bolt of Fire"] = 5.2,
   ["Simulacrum"] = 101,
+
   ["Bolt of Cold"] = 5.2,
   ["Sleep"] = 101,
+
   ["Iron Shot"] = 5.2,
   ["Slow"] = 101,
+
   ["Poison Arrow"] = 5.2,
   ["Summon Horrible Things"] = 101,
+
   ["Bolt of Draining"] = 5.2,
   ["Throw Icicle"] = 101,
+
   ["Lightning Bolt"] = 5.2,
}
+
 
 +
-- 7.5% chance to have a particular one of these spells
 +
-- (3/4 chance to be a spellcaster, then 1/2 chance to choose one of them, then there are 5 possible spells)
 +
  ["Haste"] = 7.5,
 +
  ["Silence"] = 7.5,
 +
  ["Invisibility"] = 7.5,
 +
   ["Blink"] = 7.5,
 +
   ["Blink Range"] = 7.5,
 +
 
 +
-- 2.8% chance to have a particular one of these spells
 +
-- (3/4 chance to be a spellcaster, then 9/10 to choose one of either summon or aoe,
 +
--  then 1/4 chance to choose one of either a summon spell or a summon (greater) demon spell,
 +
--  then there are 6 possible spells)
 +
   ["Haunt"] = 2.8,
 +
   ["Malign Gateway"] = 2.8,
 +
  ["Summon Dragon"] = 2.8,
 +
  ["Summon Horrible Things"] = 2.8,
 +
   ["Summon Eyeballs"] = 2.8,
 +
   ["Summon Vermin"] = 2.8,
 +
 
 +
-- ... and then there's a 1/2 chance to get this spell
 +
  ["Blink Allies Encircling"] = 1.4,
 +
 
 +
-- 8.4% chance to have a particular one of these two spells
 +
-- (3/4 chance to be a spellcaster, then 9/10 to choose one of either summon or aoe,
 +
--  then 1/4 chance to choose one of either a summon spell or a summon (greater) demon spell,
 +
--  then there are 2 possible spells)
 +
   ["Summon Demon"] = 8.4,
 +
   ["Summon Greater Demon"] = 8.4,
 +
 
 +
-- 3.4% chance to have a particular one of these spells
 +
-- (3/4 chance to be a spellcaster, then 1/2 chance to choose one of them, then there are 11 possible spells)
 +
   ["Dispel Undead Range"] = 3.4,
 +
   ["Paralyse"] = 3.4,
 +
  ["Sleep"] = 3.4,
 +
   ["Mass Confusion"] = 3.4,
 +
   ["Drain Magic"] = 3.4,
 +
   ["Petrify"] = 3.4,
 +
   ["Polymorph"] = 3.4,
 +
   ["Force Lance"] = 3.4,
 +
   ["Slow"] = 3.4,
 +
   ["Sentinel's Mark"] = 3.4,
 +
   ["Dimension Anchor"] = 3.4,
  
local panlord_spells = {
+
-- 2.8% chance to have one of these spells
  ["Fire Storm"] = 101,
+
-- (1/4 chance to be a non-caster, then 1/3 chance to choose one of them, then there are 3 possible spells)
  Glaciate = 101,
+
   ["Blinkbolt"] = 2.8,
  ["Lehudib's Crystal Spear"] = 101,
+
   ["Blink Close"] = 2.8,
  ["Chain Lightning"] = 101,
+
   ["Harpoon Shot"] = 2.8,
  ["Orb of Destruction"] = 101,
 
  ["Corrosive Bolt"] = 101,
 
  ["Disintegrate"] = 101,
 
  ["Bolt of Fire"] = 101,
 
  ["Bolt of Cold"] = 101,
 
  ["Iron Shot"] = 101,
 
  ["Poison Arrow"] = 101,
 
  ["Bolt of Draining"] = 101,
 
  ["Quicksilver Bolt"] = 101,
 
  ["Force Lance"] = 101,
 
  Fireball = 101,
 
  ["Bolt of Magma"] = 101,
 
  ["Lee's Rapid Deconstruction"] = 101,
 
   ["Lightning Bolt"] = 101,
 
  Blinkbolt = 101,
 
  ["Venom Bolt"] = 101,
 
  Agony = 101,
 
  Sleep = 101,
 
  ["Iskendrun's Mystic Blast"] = 101,
 
  ["Sticky Flame Range"] = 101,
 
  ["Steam Ball"] = 101,
 
  ["Throw Icicle"] = 101,
 
  Airstrike = 101,
 
  Smiting = 101,
 
  ["Dazzling Spray"] = 101,
 
  ["Stone Arrow"] = 101,
 
  ["Static Discharge"] = 101,
 
  ["Vampiric Draining"] = 101,
 
  ["Throw Flame"] = 101,
 
  ["Throw Frost"] = 101,
 
  ["Summon Demon"] = 101,
 
  ["Summon Dragon"] = 101,
 
  ["Summon Horrible Things"] = 101,
 
  ["Summon Greater Demon"] = 101,
 
  Haunt = 101,
 
  ["Summon Hydra"] = 101,
 
  ["Malign Gateway"] = 101,
 
  Haste = 101,
 
  Invisibility = 101,
 
  ["Symbol of Torment"] = 101,
 
  ["Monstrous Menagerie"] = 101,
 
  Silence = 101,
 
  ["Shadow Creatures"] = 101,
 
   ["Summon Vermin"] = 101,
 
  ["Summon Swarm"] = 101,
 
  ["Iskenderun's Battlesphere"] = 101,
 
  ["Fulminant Prism"] = 101,
 
  ["Summon Ice Beast"] = 101,
 
  Swiftness = 101,
 
  Blink = 101,
 
  ["Summon Butterflies"] = 101,
 
  Shatter = 101,
 
  Banishment = 101,
 
   ["Freezing Cloud"] = 101,
 
  ["Poisonous Cloud"] = 101,
 
  ["Mass Confusion"] = 101,
 
  ["Metabolic Englaciation"] = 101,
 
  ["Dispel Undead"] = 101,
 
  Dig = 101,
 
  Petrify = 101,
 
  ["Olgreb's Toxic Radiance"] = 101,
 
  Paralyse = 101,
 
  Polymorph = 101,
 
  ["Mephitic Cloud"] = 101,
 
  Confuse = 101,
 
  ["Teleport Other"] = 101,
 
  Slow = 101,
 
  ["Hellfire Burst"] = 101,
 
  ["Metal Splinters"] = 101,
 
  ["Energy Bolt"] = 101,
 
  ["Orb of Electricity"] = 101,
 
  ["Hellfire"] = 101,
 
  ["Summon Eyeballs"] = 10,
 
  ["Summon Greater Demon"] = 101
 
 
}
 
}
  
 
local function is_vault_only(monster)
 
local function is_vault_only(monster)
 +
  if monster.Name == "test spawner" then return true end -- not placed in game, nor in vaults
 
   for _, flag in ipairs(monster.Flags) do
 
   for _, flag in ipairs(monster.Flags) do
 
     if flag == "Vault flag" then
 
     if flag == "Vault flag" then
Line 286: Line 277:
 
end
 
end
  
-- returns a number between 0 and 100: the percentage chance that a monster will have the spell
+
-- If a monster may be able to cast the spell, returns a table. Otherwise returns nil.
-- Monsters with no spellsets that contain the spell return 0.
+
-- The table contains the following keys:
-- Monsters with all spellsets containing the spell return 100.
+
-- * chance - the chance that the monster will have the spell. May be missing if the chance is too hard to calculate.
-- Monsters that might have the spell, but it's too hard to calculate the percentage chance, return 101.
+
-- * damage - the damage that the spell does when the monster casts it (if any)
local function monster_has_spell(monster, spell_name)
+
local function monster_spell_data(monster, spell_name)
   if monster.Species == "lich" and monster.Name ~= "Boris" then
+
  local special_table = nil
     return lich_spells[spell_name] or 0
+
   if monster.Name == "pandemonium lord" then
 +
     special_table = panlord_spells
 
   end
 
   end
  
   if monster.Name == "pandemonium lord" then
+
   if special_table then
     return panlord_spells[spell_name] or 0
+
    local chance = special_table[spell_name]
 +
    if chance then
 +
      if type(chance) == "number" then
 +
        return {chance = chance}
 +
      else
 +
        return {}
 +
      end
 +
     else
 +
      return nil
 +
    end
 
   end
 
   end
  
 
   if not monster.Spellsets then
 
   if not monster.Spellsets then
     return 0
+
     return nil
 
   end
 
   end
  
 
   local has = 0
 
   local has = 0
 
   local total = 0
 
   local total = 0
 +
  local damage = nil
 
   for _, spellset in ipairs(monster.Spellsets) do
 
   for _, spellset in ipairs(monster.Spellsets) do
 
     total = total + 1
 
     total = total + 1
Line 310: Line 312:
 
       if spell.Spell == spell_name then
 
       if spell.Spell == spell_name then
 
         has = has + 1
 
         has = has + 1
 +
        if not damage then
 +
          damage = spell.Damage
 +
        end
 
         break
 
         break
 
       end
 
       end
 
     end
 
     end
 
   end
 
   end
   return has * 100 / total
+
   if has > 0 then
 +
    return {chance = has * 100 / total, damage = damage}
 +
  else
 +
    return nil
 +
  end
 
end
 
end
  
Line 348: Line 357:
 
   wizard = "Wizard (monster)",
 
   wizard = "Wizard (monster)",
 
}
 
}
 +
 +
local function monsterlink(frame, data)
 +
  local ret =  "* " .. frame:expandTemplate{title = "monsterlink", args = {data.monster}}
 +
  if data.damage or data.chance and data.chance ~= 100 then
 +
    ret = ret .. " ("
 +
    if data.damage then
 +
      ret = ret .. data.damage .. " damage"
 +
      if data.chance and data.chance ~= 100 then
 +
        ret = ret .. ", "
 +
      end
 +
    end
 +
    if data.chance and data.chance ~= 100 then
 +
      ret = ret .. ("%.1f%% chance"):format(data.chance)
 +
    end
 +
    ret = ret .. ")"
 +
  end
 +
  return ret
 +
end
  
 
function p.monsters_with_spell(frame)
 
function p.monsters_with_spell(frame)
Line 354: Line 381:
 
   local sometimes = {}
 
   local sometimes = {}
 
   for monster, monsterdata in pairs(data) do
 
   for monster, monsterdata in pairs(data) do
     local spell_chance = monster_has_spell(monsterdata, spell_name)
+
     local data = not is_vault_only(monsterdata) and monster_spell_data(monsterdata, spell_name)
     if spell_chance > 0 and not is_vault_only(monsterdata) then
+
     if data then
       monster = disambig[monster] or (monster:gsub("^%l", string.upper))
+
       data.monster = disambig[monster] or (monster:gsub("^%l", string.upper))
       if spell_chance == 100 then
+
       if data.chance and data.chance == 100 then
         table.insert(always, monster)
+
         table.insert(always, data)
 
       else
 
       else
         table.insert(sometimes, {monster, spell_chance})
+
         table.insert(sometimes, data)
 
       end
 
       end
 
     end
 
     end
 
   end
 
   end
   table.sort(always, string_icmp)
+
   table.sort(always, function (m1, m2) return string_icmp(m1.monster, m2.monster) end)
   table.sort(sometimes, function(m1, m2) return string_icmp(m1[1], m2[1]) end)
+
   table.sort(sometimes, function(m1, m2) return string_icmp(m1.monster, m2.monster) end)
 
   local ret = {}
 
   local ret = {}
 
   if #always > 0 then
 
   if #always > 0 then
 
     table.insert(ret, "The following enemies cast " .. spell_name .. ":")
 
     table.insert(ret, "The following enemies cast " .. spell_name .. ":")
     for _, monster in ipairs(always) do
+
     for _, data in ipairs(always) do
       table.insert(ret, "* " .. frame:expandTemplate{title = "monsterlink", args = {monster}})
+
       table.insert(ret, monsterlink(frame, data))
 
     end
 
     end
 
   end
 
   end
 
   if #sometimes > 0 then
 
   if #sometimes > 0 then
 
     table.insert(ret, "The following enemies may be able to cast " .. spell_name .. ", depending on their spell set:")
 
     table.insert(ret, "The following enemies may be able to cast " .. spell_name .. ", depending on their spell set:")
     for _, monster in ipairs(sometimes) do
+
     for _, data in ipairs(sometimes) do
      local name = monster[1]
+
       table.insert(ret, monsterlink(frame, data))
      local chance = monster[2]
 
      local bullet = "* " .. frame:expandTemplate{title = "monsterlink", args = {name}}
 
      if chance ~= 101 then
 
        bullet = bullet .. (" (%.1f%% chance)"):format(monster[2])
 
      end
 
       table.insert(ret, bullet)
 
 
     end
 
     end
 
   end
 
   end

Latest revision as of 21:42, 23 March 2024

Documentation for this module may be created at Module:Monster/doc

local p = {}
local data = mw.loadData('Module: Table of monsters')

-- Taken from max_corpse_chunks in mon-util.cc
local max_corpse_chunks = {
  Tiny = 1,
  Little = 2,
  Small = 3,
  Medium = 4,
  Large = 9,
  Big = 10,
  Giant = 12,
}

local function name_arg(frame)
  local name = frame.args[1]
  if not name or name == "" then
    name = mw.title.getCurrentTitle().text
  end
  return name
end

local function tile_name_arg(frame)
  local tile_name = frame.args[2]
  local monster_name = frame.args[1]
  if not tile_name or tile_name == "" then
    tile_name = monster_name
  end
  if not tile_name or tile_name == "" then
    tile_name = mw.title.getCurrentTitle().text
  end
  return tile_name
end

function p.glyph(frame)
  local name = name_arg(frame)
  local monster = data[name] or data[name:lower()]
  if monster then
    return frame:expandTemplate{title = monster.Colour, args = {monster.Glyph}}
  else
    -- probably obsolete monster; fall back to old templates
    return frame:expandTemplate{title = "glyph/" .. name}
  end
end

function p.monster_info(frame)
  local name = name_arg(frame)
  local monster = data[name] or data[name:lower()]
  if not monster then
    return name
  end
  local args = {}
  args.name = monster.Name
  args.tile_name = tile_name_arg(frame)
  args.glyph = frame:expandTemplate{title = monster.Colour, args = {monster.Glyph}}

  local flags = {}
  for i, v in ipairs(monster.Flags) do
    flags[i] = frame:expandTemplate{title = v}
  end
  args.flags = table.concat(flags, "<br>")

  local resistances = {}
  for i, v in ipairs(monster.Resistances) do
    resistances[i] = frame:expandTemplate{title = v}
  end
  if #resistances == 0 then
    args.resistances = "None"
  else
    args.resistances = table.concat(resistances, "<br>")
  end

  local vulnerabilities = {}
  for i, v in ipairs(monster.Vulnerabilities) do
    vulnerabilities[i] = frame:expandTemplate{title = v}
  end
  if #vulnerabilities == 0 then
    args.vulnerabilities = "None"
  else
    args.vulnerabilities = table.concat(vulnerabilities, "<br>")
  end

  if monster.Corpse ~= "No" then
    args.max_chunks = max_corpse_chunks[monster.Size] or 0
  else
    args.max_chunks = 0
  end
  args.meat = frame:expandTemplate{title = monster.Corpse .. " corpse"}
  args.xp = monster.XP
  args.holiness = frame:expandTemplate{title = monster.Holiness}
  args.magic_resistance = monster.MR

  local avg_hp = monster["Average HP 10x"] / 10
  local hp_min = math.max(math.floor(0.66 * avg_hp), 1)
  local hp_max = math.ceil(1.33 * avg_hp)
  args.hp_range = ("%d-%d"):format(hp_min, hp_max)
  args.avg_hp = avg_hp

  args.armour_class = monster.AC
  args.evasion = monster.EV
  args.habitat = monster.Habitat
  args.speed = monster.Speed
  args.size = frame:expandTemplate{title = monster.Size}

  local item_use = {}
  for i, v in ipairs(monster["Item Use"]) do
    item_use[i] = frame:expandTemplate{title = v}
  end
  args.item_use = table.concat(item_use, "<br>")

  for i = 1, 4 do
    local attack = monster.Attacks[i]
    if attack then
      local typ = frame:expandTemplate{title = attack.Type .. " type"}
      local flavour = frame:expandTemplate{title = attack.Flavour .. " flavour"}
      args["attack" .. i] = ("%d (%s: %s)"):format(attack.Damage, typ, flavour)
    else
      args["attack" .. i] = ""
    end
  end

  args.hit_dice = monster.HD
  args.base_hp = monster["Base HP"]
  args.extra_hp = monster["Rand HP"]
  args.fixed_hp = monster["Fixed HP"]
  args.intelligence = frame:expandTemplate{title = monster.Intelligence .. " intelligence"}
  args.genus = monster.Genus
  args.species = monster.Species

  local infobox = frame:expandTemplate{title = "monster", args = args}

  local flavour = monster.Description
  if monster.Quote then
    flavour = flavour .. "\n----\n" .. monster.Quote:gsub("\n", "<br>")
  end
  flavour = frame:expandTemplate{title = "flavour", args = {flavour}}
  return infobox .. "\n" .. flavour
end

local spell_disambig = {
  ["Phantom Mirror"] = "Phantom Mirror (spell)",
}

local function spell_link(name)
    if spell_disambig[name] then
        return '[[' .. spell_disambig[name] .. '|' .. name .. ']]'
    else
        return '[[' .. name .. ']]'
    end
end

local roman = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"}

function p.monster_spells(frame)
  local name = name_arg(frame)
  local monster = data[name] or data[name:lower()]

  local ret = "==Spells==\n[[Category:Spellcaster]]"
  local cleared = false
  for i, spells in ipairs(monster.Spellsets) do
    ret = ret .. [=[<div style="margin: 1.0em; margin-top:0; margin-right:0; padding: 0px; float: left; border:none;">
{| class="prettytable" style="border:none; margin:0; padding:0; width:28em;"
! colspan="3" style="font-size:larger;" | Spell set ]=] .. roman[i] .. "\n"
    for j, spell in ipairs(spells) do
      ret = ret .. '|-\n! align="left" | Slot<sup>' .. j .. '</sup>\n| ' .. spell_link(spell.Spell)
      if spell.Damage then
        ret = ret .. " (" .. spell.Damage .. ")"
      end
      ret = ret .. '\n| '
      local flags = {}
      for i, v in ipairs(spell.Flags) do
        flags[i] = frame:expandTemplate{title = v .. " slot flag"}
      end
      ret = ret .. table.concat(flags, ",<br> ") .. "\n"
    end
    ret = ret .. "|-\n|}</div>"
    cleared = i % 2 == 0
    if cleared then
      ret = ret .. '<div style="clear:left;"></div>'
    end
  end
  if not cleared then
    ret = ret .. '<div style="clear:left;"></div>'
  end
  return ret
end

local panlord_spells = {
-- 3.4% chance to have a particular one of these spells
-- (3/4 chance to be a spellcaster, then 9/10 chance to have either summon or aoe,
--  then 1/2 chance to choose an aoe spell, then there are 10 possible spells)
  ["Symbol of Torment"] = 3.4,
  ["Fire Storm"] = 3.4,
  ["Glaciate"] = 3.4,
  ["Chain Lightning"] = 3.4,
  ["Freezing Cloud"] = 3.4,
  ["Poisonous Cloud"] = 3.4,
  ["Metal Splinters"] = 3.4,
  ["Bolt of Devastation"] = 3.4,
  ["Orb of Electricity"] = 3.4,
  ["Conjure Ball Lightning"] = 3.4,

-- 5.2% chance to have a particular one of these spells 
-- (3/4 chance to be a spellcaster, then 9/10 chance to choose one of them, then there are 13 possible spells)
  ["Call Down Damnation"] = 5.2,
  ["Lehudib's Crystal Spear"] = 5.2,
  ["Corrosive Bolt"] = 5.2,
  ["Quicksilver Bolt"] = 5.2,
  ["Orb of Destruction"] = 5.2,
  ["Energy Bolt"] = 5.2,
  ["Mindburst"] = 5.2,
  ["Bolt of Fire"] = 5.2,
  ["Bolt of Cold"] = 5.2,
  ["Iron Shot"] = 5.2,
  ["Poison Arrow"] = 5.2,
  ["Bolt of Draining"] = 5.2,
  ["Lightning Bolt"] = 5.2,

-- 7.5% chance to have a particular one of these spells
-- (3/4 chance to be a spellcaster, then 1/2 chance to choose one of them, then there are 5 possible spells)
  ["Haste"] = 7.5,
  ["Silence"] = 7.5,
  ["Invisibility"] = 7.5,
  ["Blink"] = 7.5,
  ["Blink Range"] = 7.5,

-- 2.8% chance to have a particular one of these spells
-- (3/4 chance to be a spellcaster, then 9/10 to choose one of either summon or aoe,
--  then 1/4 chance to choose one of either a summon spell or a summon (greater) demon spell,
--  then there are 6 possible spells)
  ["Haunt"] = 2.8,
  ["Malign Gateway"] = 2.8,
  ["Summon Dragon"] = 2.8,
  ["Summon Horrible Things"] = 2.8,
  ["Summon Eyeballs"] = 2.8,
  ["Summon Vermin"] = 2.8,

-- ... and then there's a 1/2 chance to get this spell
  ["Blink Allies Encircling"] = 1.4,

-- 8.4% chance to have a particular one of these two spells
-- (3/4 chance to be a spellcaster, then 9/10 to choose one of either summon or aoe,
--  then 1/4 chance to choose one of either a summon spell or a summon (greater) demon spell,
--  then there are 2 possible spells)
  ["Summon Demon"] = 8.4,
  ["Summon Greater Demon"] = 8.4,

-- 3.4% chance to have a particular one of these spells
-- (3/4 chance to be a spellcaster, then 1/2 chance to choose one of them, then there are 11 possible spells)
  ["Dispel Undead Range"] = 3.4,
  ["Paralyse"] = 3.4,
  ["Sleep"] = 3.4,
  ["Mass Confusion"] = 3.4,
  ["Drain Magic"] = 3.4,
  ["Petrify"] = 3.4,
  ["Polymorph"] = 3.4,
  ["Force Lance"] = 3.4,
  ["Slow"] = 3.4,
  ["Sentinel's Mark"] = 3.4,
  ["Dimension Anchor"] = 3.4,

-- 2.8% chance to have one of these spells
-- (1/4 chance to be a non-caster, then 1/3 chance to choose one of them, then there are 3 possible spells)
  ["Blinkbolt"] = 2.8,
  ["Blink Close"] = 2.8,
  ["Harpoon Shot"] = 2.8,
}

local function is_vault_only(monster)
  if monster.Name == "test spawner" then return true end -- not placed in game, nor in vaults
  for _, flag in ipairs(monster.Flags) do
    if flag == "Vault flag" then
      return true
    end
  end
  return false
end

-- If a monster may be able to cast the spell, returns a table. Otherwise returns nil.
-- The table contains the following keys:
-- * chance - the chance that the monster will have the spell. May be missing if the chance is too hard to calculate.
-- * damage - the damage that the spell does when the monster casts it (if any)
local function monster_spell_data(monster, spell_name)
  local special_table = nil
  if monster.Name == "pandemonium lord" then
    special_table = panlord_spells
  end

  if special_table then
    local chance = special_table[spell_name]
    if chance then
      if type(chance) == "number" then
        return {chance = chance}
      else
        return {}
      end
    else
      return nil
    end
  end

  if not monster.Spellsets then
    return nil
  end

  local has = 0
  local total = 0
  local damage = nil
  for _, spellset in ipairs(monster.Spellsets) do
    total = total + 1
    for _, spell in ipairs(spellset) do
      if spell.Spell == spell_name then
        has = has + 1
        if not damage then
          damage = spell.Damage
        end
        break
      end
    end
  end
  if has > 0 then
    return {chance = has * 100 / total, damage = damage}
  else
    return nil
  end
end

local function string_icmp(s1, s2)
  return s1:lower() < s2:lower()
end

local disambig = {
  centaur = "Centaur (monster)",
  ["death knight"] = "Death knight (monster)",
  ["deep dwarf"] = "Deep dwarf (monster)",
  demigod = "Demigod (monster)",
  demonspawn = "Demonspawn (monster)",
  draconian = "Draconian (monster)",
  felid = "Felid (monster)",
  formicid = "Formicid (monster)",
  gargoyle = "Gargoyle (monster)",
  ghoul = "Ghoul (monster)",
  halfling = "Halfling (monster)",
  human = "Human (monster)",
  kobold = "Kobold (monster)",
  merfolk = "Merfolk (monster)",
  minotaur = "Minotaur (monster)",
  mummy = "Mummy (monster)",
  naga = "Naga (monster)",
  necromancer = "Necromancer (monster)",
  octopode = "Octopode (monster)",
  ogre = "Ogre (monster)",
  spriggan = "Spriggan (monster)",
  troll = "Troll (monster)",
  tengu = "Tengu (monster)",
  vampire = "Vampire (monster)",
  wizard = "Wizard (monster)",
}

local function monsterlink(frame, data)
  local ret =  "* " .. frame:expandTemplate{title = "monsterlink", args = {data.monster}}
  if data.damage or data.chance and data.chance ~= 100 then
    ret = ret .. " ("
    if data.damage then
      ret = ret .. data.damage .. " damage"
      if data.chance and data.chance ~= 100 then
        ret = ret .. ", "
      end
    end
    if data.chance and data.chance ~= 100 then
      ret = ret .. ("%.1f%% chance"):format(data.chance)
    end
    ret = ret .. ")"
  end
  return ret
end

function p.monsters_with_spell(frame)
  local spell_name = name_arg(frame)
  local always = {}
  local sometimes = {}
  for monster, monsterdata in pairs(data) do
    local data = not is_vault_only(monsterdata) and monster_spell_data(monsterdata, spell_name)
    if data then
      data.monster = disambig[monster] or (monster:gsub("^%l", string.upper))
      if data.chance and data.chance == 100 then
        table.insert(always, data)
      else
        table.insert(sometimes, data)
      end
    end
  end
  table.sort(always, function (m1, m2) return string_icmp(m1.monster, m2.monster) end)
  table.sort(sometimes, function(m1, m2) return string_icmp(m1.monster, m2.monster) end)
  local ret = {}
  if #always > 0 then
    table.insert(ret, "The following enemies cast " .. spell_name .. ":")
    for _, data in ipairs(always) do
      table.insert(ret, monsterlink(frame, data))
    end
  end
  if #sometimes > 0 then
    table.insert(ret, "The following enemies may be able to cast " .. spell_name .. ", depending on their spell set:")
    for _, data in ipairs(sometimes) do
      table.insert(ret, monsterlink(frame, data))
    end
  end
  return table.concat(ret, "\n")
end

return p