Difference between revisions of "Module:Monster"
(Fix formatting of attacks) |
Patrick2011b (talk | contribs) m (Energy Bolt -> Bolt of Devastation) |
||
(42 intermediate revisions by 5 users not shown) | |||
Line 13: | Line 13: | ||
} | } | ||
− | function | + | local function name_arg(frame) |
local name = frame.args[1] | local name = frame.args[1] | ||
if not name or name == "" then | if not name or name == "" then | ||
name = mw.title.getCurrentTitle().text | name = mw.title.getCurrentTitle().text | ||
end | 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()] | local monster = data[name] or data[name:lower()] | ||
if not monster then | if not monster then | ||
Line 24: | 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 62: | Line 91: | ||
args.magic_resistance = monster.MR | args.magic_resistance = monster.MR | ||
− | local | + | local avg_hp = monster["Average HP 10x"] / 10 |
− | local hp_max = | + | local hp_min = math.max(math.floor(0.66 * avg_hp), 1) |
− | args.hp_range = ("%d-%d") | + | local hp_max = math.ceil(1.33 * avg_hp) |
− | args.avg_hp = | + | args.hp_range = ("%d-%d"):format(hp_min, hp_max) |
+ | args.avg_hp = avg_hp | ||
args.armour_class = monster.AC | args.armour_class = monster.AC | ||
Line 98: | Line 128: | ||
args.species = monster.Species | args.species = monster.Species | ||
− | return frame:expandTemplate{title = "monster", args = | + | 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 | end | ||
return p | return p |
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