Module:Monster: Difference between revisions
From Ashenzari's Archive
More actions
imported>Edsrzf Fix typo |
local override: re-apply Ashenzari's Archive patch (see wiki/local-overrides/) |
||
| (53 intermediate revisions by 9 users not shown) | |||
| Line 1: | Line 1: | ||
local p = {} | local p = {} | ||
local data = mw.loadData('Module: Table of monsters') | local data = mw.loadData('Module: Table of monsters') | ||
local spells_table = mw.loadData('Module: Table of spells') | |||
local monster_spells_table = mw.loadData('Module: Table of monster spells') | |||
local function name_arg(frame) | |||
local | |||
function | |||
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 43: | ||
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 52: | Line 72: | ||
end | end | ||
args.xp = monster.XP | args.xp = monster.XP | ||
args.holiness = frame:expandTemplate{title = monster.Holiness} | args.holiness = frame:expandTemplate{title = monster.Holiness} | ||
args. | args.willpower = monster.Willpower | ||
local | local avg_hp = monster["Average HP 10x"] / 10 | ||
local | 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.avg_hp = | |||
args.armour_class = monster.AC | args.armour_class = monster.AC | ||
| Line 111: | Line 122: | ||
return infobox .. "\n" .. flavour | return infobox .. "\n" .. flavour | ||
end | 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 function spell_tile(name) | |||
if spell_disambig[name] then | |||
-- The upstream spell icon's filename includes literal quotation marks. | |||
return '[[File:"' .. spell_disambig[name]:lower() .. '".png]]' | |||
else | |||
return '[[File:' .. name:lower() .. '.png]]' | |||
end | |||
end | |||
local roman = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"} | |||
function p.monster_spells(frame) | function p.monster_spells(frame) | ||
local name = frame | local name = name_arg(frame) | ||
local monster = data[name] or data[name:lower()] | local monster = data[name] or data[name:lower()] | ||
local ret = "==Spells==\n[[Category:Spellcaster]]" | local ret = "==Spells==\n[[Category:Spellcaster]]" | ||
local cleared = false | |||
local has_spellsets = (monster.Spellsets[2] ~= nil) | |||
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: | {| class="prettytable" style="border:none; margin:0; padding:0; width:28em;" | ||
! colspan=" | ]=] | ||
if has_spellsets then | |||
ret = ret .. '! colspan="5" style="font-size:larger;" | Spell set ' .. roman[i] .. "\n|-\n" | |||
end | |||
ret = ret .. "! Tile !! Name !! Damage !! Range !! Flags\n" | |||
for j, spell in ipairs(spells) do | for j, spell in ipairs(spells) do | ||
ret = ret .. '|-\n | ret = ret .. '|-\n| ' .. spell_tile(spell.Spell) .. '\n' | ||
ret = ret .. '| ' .. spell_link(spell.Spell) .. '\n' | |||
ret = ret .. "| " .. (spell.Damage or '—') .. "\n" | |||
local spell_data = spells_table[spell.Spell] | |||
if spell_data == nil then | |||
spell_data = monster_spells_table[spell.Spell] | |||
end | |||
local rng = '—' | |||
if spell_data ~= nil then | |||
if spell_data.range ~= nil then | |||
rng = spell_data.range.max | |||
if rng == 8 then | |||
rng = 'LOS' | |||
end | |||
end | |||
end | |||
ret = ret .. '| ' .. rng .. '\n' | |||
local flags = {} | local flags = {} | ||
for i, v in ipairs(spell.Flags) do | for i, v in ipairs(spell.Flags) do | ||
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 | |||
if cleared then | |||
ret = ret .. '<div style="clear:left;"></div>' | |||
end | |||
end | |||
if not cleared then | |||
ret = ret .. '<div style="clear:left;"></div>' | |||
end | end | ||
return ret | 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)", | |||
["hellfire mortar"] = "Hellfire mortar (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_link, 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 = monster:gsub("^%l", string.upper) | |||
data.monster_link = disambig[monster] or data.monster | |||
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 | |||
function p.categorize_will(frame) | |||
local will = frame.args[1] | |||
if not will then | |||
return '' | |||
end | |||
if will == 'Immune' then | |||
return frame:expandTemplate{title = "Will Immune"} | |||
end | |||
local will_value = tonumber(will) | |||
if not will_value then | |||
return will | |||
end | |||
if will_value == 0 then | |||
return frame:expandTemplate{title = "Will None"} | |||
end | |||
local threshold = 0 | |||
local step = 40 | |||
local title = 'Will' | |||
while threshold < 200 do | |||
threshold = threshold + step | |||
title = title..'+' | |||
if will_value <= threshold then | |||
return frame:expandTemplate{title = title, args={will}} | |||
end | |||
end | |||
return frame:expandTemplate{title = 'Will+++++', args={will}} | |||
end | end | ||
return p | return p | ||
Latest revision as of 18:45, 27 September 2026
Module, which is used by various monster templates, such as Template:Monster info, Template:Monster spells
local p = {}
local data = mw.loadData('Module: Table of monsters')
local spells_table = mw.loadData('Module: Table of spells')
local monster_spells_table = mw.loadData('Module: Table of monster spells')
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
args.xp = monster.XP
args.holiness = frame:expandTemplate{title = monster.Holiness}
args.willpower = monster.Willpower
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 function spell_tile(name)
if spell_disambig[name] then
-- The upstream spell icon's filename includes literal quotation marks.
return '[[File:"' .. spell_disambig[name]:lower() .. '".png]]'
else
return '[[File:' .. name:lower() .. '.png]]'
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
local has_spellsets = (monster.Spellsets[2] ~= nil)
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;"
]=]
if has_spellsets then
ret = ret .. '! colspan="5" style="font-size:larger;" | Spell set ' .. roman[i] .. "\n|-\n"
end
ret = ret .. "! Tile !! Name !! Damage !! Range !! Flags\n"
for j, spell in ipairs(spells) do
ret = ret .. '|-\n| ' .. spell_tile(spell.Spell) .. '\n'
ret = ret .. '| ' .. spell_link(spell.Spell) .. '\n'
ret = ret .. "| " .. (spell.Damage or '—') .. "\n"
local spell_data = spells_table[spell.Spell]
if spell_data == nil then
spell_data = monster_spells_table[spell.Spell]
end
local rng = '—'
if spell_data ~= nil then
if spell_data.range ~= nil then
rng = spell_data.range.max
if rng == 8 then
rng = 'LOS'
end
end
end
ret = ret .. '| ' .. rng .. '\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)",
["hellfire mortar"] = "Hellfire mortar (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_link, 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 = monster:gsub("^%l", string.upper)
data.monster_link = disambig[monster] or data.monster
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
function p.categorize_will(frame)
local will = frame.args[1]
if not will then
return ''
end
if will == 'Immune' then
return frame:expandTemplate{title = "Will Immune"}
end
local will_value = tonumber(will)
if not will_value then
return will
end
if will_value == 0 then
return frame:expandTemplate{title = "Will None"}
end
local threshold = 0
local step = 40
local title = 'Will'
while threshold < 200 do
threshold = threshold + step
title = title..'+'
if will_value <= threshold then
return frame:expandTemplate{title = title, args={will}}
end
end
return frame:expandTemplate{title = 'Will+++++', args={will}}
end
return p