Difference between revisions of "Module:Monster"

From CrawlWiki
Jump to: navigation, search
(Change floor to ceil and vice versa in the hp_max / hp_min code. (Discussion has determined that max HP values on the wiki are otherwise correct theoretically))
(Undo revision 49199 by NormalPerson7 (talk) Turns out this was right after all.)
Line 79: Line 79:
  
 
   local avg_hp = monster["Average HP 10x"] / 10
 
   local avg_hp = monster["Average HP 10x"] / 10
   local hp_min = math.ceil(0.66 * avg_hp)
+
   local hp_min = math.floor(0.66 * avg_hp)
   local hp_max = math.floor(1.33 * avg_hp)
+
   local hp_max = math.ceil(1.33 * avg_hp)
 
   args.hp_range = ("%d-%d"):format(hp_min, hp_max)
 
   args.hp_range = ("%d-%d"):format(hp_min, hp_max)
 
   args.avg_hp = avg_hp
 
   args.avg_hp = avg_hp

Revision as of 00:09, 9 August 2018

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

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.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.floor(0.66 * avg_hp)
  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.8% 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 9 possible spells)
  ["Symbol of Torment"] = 3.8,
  ["Fire Storm"] = 3.8,
  ["Glaciate"] = 3.8,
  ["Chain Lightning"] = 3.8,
  ["Freezing Cloud"] = 3.8,
  ["Poisonous Cloud"] = 3.8,
  ["Metal Splinters"] = 3.8,
  ["Energy Bolt"] = 3.8,
  ["Orb of Electricity"] = 3.8,

-- 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,
  ["Disintegrate"] = 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,
  ["Blinkbolt"] = 7.5,

-- 4.2% 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/2 chance to choose a summon spell, then there are 8 possible spells)
  ["Haunt"] = 4.2,
  ["Malign Gateway"] = 4.2,
  ["Summon Dragon"] = 4.2,
  ["Summon Horrible Things"] = 4.2,
  ["Shadow Creatures"] = 4.2,
  ["Summon Eyeballs"] = 4.2,
  ["Summon Vermin"] = 4.2,
  ["Summon Butterflies"] = 4.2,

-- 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 9 possible spells)
  ["Dispel Undead"] = 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,

-- 18.8% 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 2 possible spells)
  ["Summon Demon"] = 18.8,
  ["Summon Greater Demon"] = 18.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