Commit b823d5e3c1b320f94c39e8b5d3c8023888ce0e50

Authored by DarkGod
2 parents cdbe4bb4 c0d173b7

Merge branch 'Shibari/t-engine4-1.6BossChanges'

... ... @@ -769,7 +769,8 @@ function _M:addEntity(level, e, typ, x, y, no_added)
769 769 if x and y then level.map(x, y, Map.TRIGGER, e) end
770 770 end
771 771 e:check("addedToLevel", level, x, y)
772   - e:check("on_added", level, x, y)
  772 + e:check("on_added", level, x, y) -- Sustains are activated here
  773 + e:check("on_added_final", level, x, y)
773 774 end
774 775
775 776 --- If we are loaded we need a new uid
... ...
... ... @@ -1485,6 +1485,76 @@ function _M:entityFilter(zone, e, filter, type)
1485 1485 end
1486 1486 end
1487 1487
  1488 +-- Randbosses tend to cause problems early game so we check for problematic cases and reduce their power at low levels here
  1489 +-- Called when adding to level after the actor is fully resolved and sustains are activated (these caps are often hit because of sustains)
  1490 +local standard_rnd_boss_adjust = function(b)
  1491 + if b.level <= 30 then
  1492 + -- Damage reduction is applied in all cases, acknowledging the frontloaded strength of randbosses and the potential for players to lack tools early
  1493 + b.inc_damage = b.inc_damage or {}
  1494 + local change = (70 * (30 - b.level + 1) / 30) + 20
  1495 + b.inc_damage.all = math.max(-80, (b.inc_damage.all or 0) - change)
  1496 +
  1497 + -- Things prone to binary outcomes (0 damage, 0 hit rate, ...) like armor and defense are only reduced if they exceed a cap per level regardless of source
  1498 + -- This lets us not worry about stuff like Shield Wall+lucky equipment creating early threats that some builds cannot hurt
  1499 + -- Note that while these seem strict they are *not* saying these values are unreasonable early for anything, they're saying they're unreasonable for randbosses specifically
  1500 + local max = b.level / 2
  1501 + local flat = b:combatGetFlatResist()
  1502 + change = (max - flat)
  1503 + if flat > max then
  1504 + b.flat_damage_armor.all = b.flat_damage_armor.all - (flat - max)
  1505 + print("[standard_rnd_boss_adjust]: Adjusting flat armor", flat, "Max", max, "Change", change)
  1506 + end
  1507 +
  1508 + if b.level <= 20 then
  1509 + local armor = b:combatArmor()
  1510 + max = b.level
  1511 + change = (max - armor)
  1512 + if armor > max then
  1513 + b.combat_armor = b.combat_armor - (armor - max)
  1514 + print("[standard_rnd_boss_adjust]: Adjusting armor", armor, "Max", max, "Change", change)
  1515 + end
  1516 +
  1517 + local defense = b:combatDefense()
  1518 + max = b.level
  1519 + change = (max - defense)
  1520 + if defense > max then
  1521 + b.combat_def = b.combat_def - (defense - max)
  1522 + print("[standard_rnd_boss_adjust]: Adjusting defense", defense, "Max", max, "Change", change)
  1523 + end
  1524 +
  1525 + -- Temporarily just hard removing this early game pending this stat not being spammed everywhere causing tons of damage most people don't even notice is happening
  1526 + local retal = rng.table(table.listify(b.on_melee_hit))
  1527 + if retal then
  1528 + b.on_melee_hit = {}
  1529 + --b.on_melee_hit[retal[1]] = retal[2]
  1530 + end
  1531 + end
  1532 +
  1533 + -- Early game melee don't have much mobility which makes randbosses too good at running and pulling more enemies in or being generally frustrating
  1534 + b.ai_tactic.escape = -1
  1535 +
  1536 + -- Cap the talent level of crippling debuffs (stun, ...) at 1 + floor(level / 10)
  1537 + -- rnd_boss_restrict is the right way to handle this for most things
  1538 + -- Tactical tables can have a variety of structures, so we just look in all subtables for a key named "stun"
  1539 + for id, level in pairs(b.talents) do
  1540 + local talent = b:getTalentFromId(id)
  1541 + if talent and talent.tactical and _G.type(talent.tactical) == "table" then
  1542 + table.check(
  1543 + talent.tactical,
  1544 + function(t, where, v, tv)
  1545 + if tv == "string" and (v:lower() == "stun") then
  1546 + b.talents[id] = math.min(b.talents[id], math.floor(b.level / 10) + 1)
  1547 + return false
  1548 + else
  1549 + return true
  1550 + end
  1551 + end)
  1552 + end
  1553 + end
  1554 + print("[entityFilterPost]: Done nerfing randboss")
  1555 + end
  1556 +end -- End of standard_rnd_boss_adjust
  1557 +
1488 1558 --- make some changes to an entity based on its filter parameters before finishing (resolving) it
1489 1559 -- called in Zone:makeEntity by the zone.post_filter function generated when loading a zone
1490 1560 -- filter fields interpreted:
... ... @@ -1497,6 +1567,7 @@ function _M:entityFilterPost(zone, level, type, e, filter)
1497 1567 if _G.type(filter.random_boss) == "boolean" then filter.random_boss = {}
1498 1568 else filter.random_boss = table.clone(filter.random_boss, true) end
1499 1569 filter.random_boss.level = filter.random_boss.level or zone:level_adjust_level(level, zone, type)
  1570 + filter.random_boss.rnd_boss_final_adjust = filter.random_boss.rnd_boss_final_adjust or standard_rnd_boss_adjust
1500 1571 e = self:createRandomBoss(e, filter.random_boss)
1501 1572 elseif filter.random_elite and not e.unique then
1502 1573 if _G.type(filter.random_elite) == "boolean" then filter.random_elite = {}
... ... @@ -1517,13 +1588,8 @@ function _M:entityFilterPost(zone, level, type, e, filter)
1517 1588 level = lev,
1518 1589 nb_rares = filter.random_elite.nb_rares or 1,
1519 1590 check_talents_level = true,
1520   - level_by_class = true,
1521 1591 user_post = filter.post,
1522 1592 post = function(b, data)
1523   - if data.level <= 20 then
1524   - b.inc_damage = b.inc_damage or {}
1525   - b.inc_damage.all = (b.inc_damage.all or 0) - 40 * (20 - data.level + 1) / 20
1526   - end
1527 1593 -- Drop
1528 1594 for i = 1, data.nb_rares do -- generate rares as weak (1 ego) randarts with more and stronger powers
1529 1595 local fil = {lev=lev, egos=1, greater_egos_bias = 0, power_points_factor = 3, nb_powers_add = 2, forbid_power_source=b.not_power_source,
... ... @@ -1548,6 +1614,7 @@ function _M:entityFilterPost(zone, level, type, e, filter)
1548 1614 end
1549 1615 if data.user_post then data.user_post(b, data) end
1550 1616 end,
  1617 + rnd_boss_final_adjust = filter.random_elite.rnd_boss_final_adjust or standard_rnd_boss_adjust
1551 1618 }
1552 1619 e = self:createRandomBoss(e, table.merge(base, filter.random_elite, true))
1553 1620 end
... ... @@ -1913,6 +1980,426 @@ function _M:createRandomZone(zbase)
1913 1980 return zone, boss
1914 1981 end
1915 1982
  1983 +--- Add one or more character classes to an actor, updating stats, talents, and equipment
  1984 +-- @param b = actor(boss) to update
  1985 +-- @param data = optional parameters:
  1986 +-- @param data.update_body a table of inventories to add, set true to add a full suite of inventories
  1987 +-- @param data.force_classes = specific subclasses to apply first, ignoring restrictions
  1988 +-- {"Rogue", "Necromancer", Corruptor = true, Bulwark = true, ...}
  1989 +-- applied in order of numerical index, then randomly
  1990 +-- @param data.nb_classes = random classes to add (in addition to any forced classes) <2>
  1991 +-- @param data.class_filter = function(cdata, b) that must return true for any class picked.
  1992 +-- (cdata, b = subclass definition in engine.Birther.birth_descriptor_def.subclass, boss (before classes are applied))
  1993 +-- @param data.no_class_restrictions set true to skip class compatibility checks <nil>
  1994 +-- @param data.autolevel = autolevel scheme to use for stats (set false to keep current) <"random_boss">
  1995 +-- @param data.spend_points = spend any unspent stat points (after adding all classes)
  1996 +-- @param data.add_trees = {["talent tree name 1"]=true/mastery bonus, ["talent tree name 2"]=true/mastery bonus, ..} additional talent trees to learn
  1997 +-- @param data.check_talents_level set true to enforce talent level restrictions <nil>
  1998 +-- @param data.auto_sustain set true to activate sustained talents at birth <nil>
  1999 +-- @param data.forbid_equip set true to not apply class equipment resolvers or equip inventory <nil>
  2000 +-- @param data.loot_quality = drop table to use for equipment <"boss">
  2001 +-- @param data.drop_equipment set true to force dropping of equipment <nil>
  2002 +-- @param instant set true to force instant learning of talents and generating golem <nil>
  2003 +function _M:applyRandomClass(b, data, instant)
  2004 + if not data.level then data.level = b.level end
  2005 +
  2006 + ------------------------------------------------------------
  2007 + -- Apply talents from classes
  2008 + ------------------------------------------------------------
  2009 + -- Apply a class
  2010 + local Birther = require "engine.Birther"
  2011 + b.learn_tids = {}
  2012 + local function apply_class(class)
  2013 + local mclasses = Birther.birth_descriptor_def.class
  2014 + local mclass = nil
  2015 + for name, data in pairs(mclasses) do
  2016 + if data.descriptor_choices and data.descriptor_choices.subclass and data.descriptor_choices.subclass[class.name] then mclass = data break end
  2017 + end
  2018 + if not mclass then return end
  2019 +
  2020 + print("[applyRandomClass]", b.uid, b.name, "Adding class", class.name, mclass.name)
  2021 + -- add class to list and build inherent power sources
  2022 + b.descriptor = b.descriptor or {}
  2023 + b.descriptor.classes = b.descriptor.classes or {}
  2024 + table.append(b.descriptor.classes, {class.name})
  2025 +
  2026 + -- build inherent power sources and forbidden power sources
  2027 + -- b.forbid_power_source --> b.not_power_source used for classes
  2028 + b.power_source = table.merge(b.power_source or {}, class.power_source or {})
  2029 + b.not_power_source = table.merge(b.not_power_source or {}, class.not_power_source or {})
  2030 + -- update power source parameters with the new class
  2031 + b.not_power_source, b.power_source = self:updatePowers(self:attrPowers(b, b.not_power_source), b.power_source)
  2032 + print(" power types: not_power_source =", table.concat(table.keys(b.not_power_source),","), "power_source =", table.concat(table.keys(b.power_source),","))
  2033 +
  2034 + -- Update/initialize base stats, set stats auto_leveling
  2035 + if class.stats or b.auto_stats then
  2036 + b.stats, b.auto_stats = b.stats or {}, b.auto_stats or {}
  2037 + for stat, v in pairs(class.stats or {}) do
  2038 + local stat_id = b.stats_def[stat].id
  2039 + b.stats[stat_id] = (b.stats[stat_id] or 10) + v
  2040 + for i = 1, v do b.auto_stats[#b.auto_stats+1] = stat_id end
  2041 + end
  2042 + end
  2043 + if data.autolevel ~= false then b.autolevel = data.autolevel or "random_boss" end
  2044 +
  2045 + -- Class talent categories
  2046 + for tt, d in pairs(mclass.talents_types or {}) do b:learnTalentType(tt, true) b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d[2]) end
  2047 + for tt, d in pairs(mclass.unlockable_talents_types or {}) do b:learnTalentType(tt, true) b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d[2]) end
  2048 + for tt, d in pairs(class.talents_types or {}) do b:learnTalentType(tt, true) b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d[2]) end
  2049 + for tt, d in pairs(class.unlockable_talents_types or {}) do b:learnTalentType(tt, true) b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d[2]) end
  2050 +
  2051 + -- Non-class talent categories
  2052 + if data.add_trees then
  2053 + for tt, d in pairs(data.add_trees) do
  2054 + if not b:knowTalentType(tt) then
  2055 + if type(d) ~= "number" then d = rng.range(1, 3)*0.1 end
  2056 + b:learnTalentType(tt, true)
  2057 + b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d)
  2058 + end
  2059 + end
  2060 + end
  2061 + -- Add starting equipment
  2062 + local apply_resolvers = function(k, resolver)
  2063 + if type(resolver) == "table" and resolver.__resolver then
  2064 + if resolver.__resolver == "equip" then
  2065 + if not data.forbid_equip then
  2066 + resolver[1].id = nil
  2067 + -- Make sure we equip some nifty stuff instead of player's starting iron stuff
  2068 + for i, d in ipairs(resolver[1]) do
  2069 + d.name, d.id = nil, nil
  2070 + d.ego_chance = nil
  2071 + d.ignore_material_restriction = true
  2072 + d.forbid_power_source = table.clone(b.not_power_source, nil, {nature=true})
  2073 + d.tome_drops = data.loot_quality or "boss"
  2074 + d.force_drop = (data.drop_equipment == nil) and true or data.drop_equipment
  2075 + end
  2076 + b[#b+1] = resolver
  2077 + end
  2078 + elseif resolver._allow_random_boss then -- explicitly allowed resolver
  2079 + b[#b+1] = resolver
  2080 + end
  2081 + elseif k == "innate_alchemy_golem" then
  2082 + b.innate_alchemy_golem = true
  2083 + elseif k == "birth_create_alchemist_golem" then
  2084 + b.birth_create_alchemist_golem = resolver
  2085 + if instant then b:check("birth_create_alchemist_golem") end
  2086 + elseif k == "soul" then
  2087 + b.soul = util.bound(1 + math.ceil(data.level / 10), 1, 10) -- Does this need to scale?
  2088 + elseif k == "can_tinker" then
  2089 + b[k] = table.clone(resolver)
  2090 + end
  2091 + end
  2092 + for k, resolver in pairs(mclass.copy or {}) do apply_resolvers(k, resolver) end
  2093 + for k, resolver in pairs(class.copy or {}) do apply_resolvers(k, resolver) end
  2094 +
  2095 + -- Assign a talent resolver for class starting talents (this makes them autoleveling)
  2096 + local tres = nil
  2097 + for k, resolver in pairs(b) do if type(resolver) == "table" and resolver.__resolver and resolver.__resolver == "talents" then tres = resolver break end end
  2098 + if not tres then tres = resolvers.talents{} b[#b+1] = tres end
  2099 + for tid, v in pairs(class.talents or {}) do
  2100 + local t = b:getTalentFromId(tid)
  2101 + if not t.no_npc_use and not t.no_npc_autolevel and (not t.random_boss_rarity or rng.chance(t.random_boss_rarity)) and not (t.rnd_boss_restrict and t.rnd_boss_restrict(b, t, data) ) then
  2102 + local max = (t.points == 1) and 1 or math.ceil(t.points * 1.2)
  2103 + local step = max / 50
  2104 + tres[1][tid] = v + math.ceil(step * data.level)
  2105 + end
  2106 + end
  2107 +
  2108 + -- Select additional talents from the class
  2109 + local known_types = {}
  2110 + for tt, d in pairs(b.talents_types) do
  2111 + known_types[tt] = b:numberKnownTalent(tt)
  2112 + end
  2113 +
  2114 + local list = {}
  2115 + for _, t in pairs(b.talents_def) do
  2116 + if b.talents_types[t.type[1]] then
  2117 + if t.no_npc_use or t.not_on_random_boss then
  2118 + known_types[t.type[1]] = known_types[t.type[1]] + 1 -- allows higher tier talents to be learnt
  2119 + else
  2120 + local ok = true
  2121 + if t.rnd_boss_restrict and t.rnd_boss_restrict(b, t, data) then
  2122 + ok = false
  2123 + print("[applyRandomClass] Random boss forbade talent because of special talent restriction", t.name, t.id, data.level)
  2124 + end
  2125 + if data.check_talents_level and rawget(t, 'require') then
  2126 + local req = t.require
  2127 + if type(req) == "function" then req = req(b, t) end
  2128 + if req and req.level and util.getval(req.level, 1) > math.ceil(data.level/2) then
  2129 + print("[applyRandomClass] Random boss forbade talent because of level", t.name, t.id, data.level)
  2130 + ok = false
  2131 + end
  2132 + end
  2133 + if t.type[1]:find("/other$") then
  2134 + print("[applyRandomClass] Random boss forbase talent because category /other", t.name, t.id, t.type[1])
  2135 + ok = false
  2136 + end
  2137 + if ok then list[t.id] = true end
  2138 + end
  2139 + end
  2140 + end
  2141 +
  2142 + local nb = 4 + 0.38*data.level^.75 -- = 11 at level 50
  2143 + nb = math.max(rng.range(math.floor(nb * 0.7), math.ceil(nb * 1.3)), 1)
  2144 + print("Adding "..nb.." random class talents to boss")
  2145 +
  2146 + local count, fails = 0, 0
  2147 + while count < nb do
  2148 + local tid = rng.tableIndex(list, b.learn_tids)
  2149 + if not tid or fails > nb * 5 then break end
  2150 + local t = b:getTalentFromId(tid)
  2151 + if t then
  2152 + if t.type[2] and known_types[t.type[1]] < t.type[2] - 1 then -- not enough of talents of type
  2153 + fails = fails + 1
  2154 + else -- ok to add
  2155 + count = count + 1
  2156 + local max = (t.points == 1) and 1 or math.ceil(t.points * 1.2)
  2157 + local step = max / 50
  2158 + local lev = math.ceil(step * data.level)
  2159 + print(count, " * talent:", tid, lev)
  2160 + if instant then -- affected by game difficulty settings
  2161 + if b:getTalentLevelRaw(tid) < lev then b:learnTalent(tid, true, lev - b:getTalentLevelRaw(tid)) end
  2162 + if t.mode == "sustained" and data.auto_sustain then b:forceUseTalent(tid, {ignore_energy=true}) end
  2163 + else -- applied when added to the level (unaffected by game difficulty settings)
  2164 + b.learn_tids[tid] = lev
  2165 + end
  2166 + known_types[t.type[1]] = known_types[t.type[1]] + 1
  2167 + list[tid] = nil
  2168 + end
  2169 + else list[tid] = nil
  2170 + end
  2171 + end
  2172 + print(" ** Finished adding", count, "of", nb, "random class talents")
  2173 +
  2174 + return true
  2175 + end
  2176 +
  2177 + -- add a full set of inventories if needed
  2178 + if data.update_body then
  2179 + b.body = type(data.update_body) == "table" and data.update_body or { INVEN = 1000, QS_MAINHAND = 1, QS_OFFHAND = 1, MAINHAND = 1, OFFHAND = 1, FINGER = 2, NECK = 1, LITE = 1, BODY = 1, HEAD = 1, CLOAK = 1, HANDS = 1, BELT = 1, FEET = 1, TOOL = 1, QUIVER = 1, QS_QUIVER = 1 }
  2180 + b:initBody()
  2181 + end
  2182 +
  2183 + -- Select classes
  2184 + local classes = Birther.birth_descriptor_def.subclass
  2185 + if data.force_classes then -- apply forced classes first, by index, then in random order
  2186 + local c_list = table.clone(data.force_classes)
  2187 + local force_classes = {}
  2188 + for i, c_name in ipairs(c_list) do
  2189 + force_classes[i] = c_list[i]
  2190 + c_list[i] = nil
  2191 + end
  2192 + table.append(force_classes, table.shuffle(table.keys(c_list)))
  2193 + for i, c_name in ipairs(force_classes) do
  2194 + if classes[c_name] then
  2195 + apply_class(table.clone(classes[c_name], true))
  2196 + else
  2197 + print(" ###Forced class", c_name, "NOT DEFINED###")
  2198 + end
  2199 + end
  2200 + end
  2201 + local list = {}
  2202 + for name, cdata in ipairs(classes) do
  2203 + if not cdata.not_on_random_boss and (not cdata.random_rarity or rng.chance(cdata.random_rarity)) and (not data.class_filter or data.class_filter(cdata, b)) then list[#list+1] = cdata
  2204 + end
  2205 + end
  2206 +
  2207 + local to_apply = data.nb_classes or 2
  2208 + while to_apply > 0 do
  2209 + local c = rng.tableRemove(list)
  2210 + if not c then break end --repeat attempts until list is exhausted
  2211 + if data.no_class_restrictions or self:checkPowers(b, c) then -- recheck power restricts here to account for any previously picked classes
  2212 + if apply_class(table.clone(c, true)) then to_apply = to_apply - 1 end
  2213 + else
  2214 + print(" * class", c.name, " rejected due to power source")
  2215 + end
  2216 + end
  2217 + if data.spend_points then -- spend any remaining unspent stat points
  2218 + repeat
  2219 + local last_stats = b.unused_stats
  2220 + engine.Autolevel:autoLevel(b)
  2221 + until last_stats == b.unused_stats or b.unused_stats <= 0
  2222 + end
  2223 +end
  2224 +
  2225 +--- Creates a random Boss (or elite) actor (pre-NPC autolevel method)
  2226 +-- @param base = base actor to add classes/talents to
  2227 +-- calls _M:applyRandomClass(b, data, instant) to add classes, talents, and equipment based on class descriptors
  2228 +-- handles data.nb_classes, data.force_classes, data.class_filter, ...
  2229 +-- optional parameters:
  2230 +-- @param data.init = function(data, b) to run before generation
  2231 +-- @param data.level = minimum level range for actor generation <1>
  2232 +-- @param data.rank = rank <3.5-4>
  2233 +-- @param data.life_rating = function(b.life_rating) <1.7 * base.life_rating + 4-9>
  2234 +-- @param data.resources_boost = multiplier for maximum resource pool sizes <3>
  2235 +-- @param data.talent_cds_factor = multiplier for all talent cooldowns <1>
  2236 +-- @param data.ai = ai_type <"tactical" if rank>3 or base.ai>
  2237 +-- @param data.ai_tactic = tactical weights table for the tactical ai <nil - generated based on talents>
  2238 +-- @param data.no_loot_randart set true to not drop a randart <nil>
  2239 +-- @param data.on_die set true to run base.rng_boss_on_die and base.rng_boss_on_die_custom on death <nil>
  2240 +-- @param data.name_scheme <randart_name_rules.default>
  2241 +-- @param data.post = function(b, data) to run last to finish generation
  2242 +function _M:createRandomBoss(base, data)
  2243 + local b = base:clone()
  2244 + data = data or {level=1}
  2245 + if data.init then data.init(data, b) end
  2246 + data.nb_classes = data.nb_classes or 2
  2247 +
  2248 + if b.rnd_boss_init then b.rnd_boss_init(b, data) end -- Used for problematic randboss bases, banning classes/talents, ...
  2249 + ------------------------------------------------------------
  2250 + -- Basic stuff, name, rank, ...
  2251 + ------------------------------------------------------------
  2252 + local ngd, name
  2253 + if base.random_name_def then
  2254 + ngd = NameGenerator2.new("/data/languages/names/"..base.random_name_def:gsub("#sex#", base.female and "female" or "male")..".txt")
  2255 + name = ngd:generate(nil, base.random_name_min_syllables, base.random_name_max_syllables)
  2256 + else
  2257 + ngd = NameGenerator.new(randart_name_rules.default)
  2258 + name = ngd:generate()
  2259 + end
  2260 + if data.name_scheme then
  2261 + b.name = data.name_scheme:gsub("#rng#", name):gsub("#base#", b.name)
  2262 + else
  2263 + b.name = name.." the "..b.name
  2264 + end
  2265 + print("[createRandomBoss] Creating random boss ", b.name, data.level, "level", data.nb_classes, "classes")
  2266 + if data.force_classes then print(" * force_classes:", (string.fromTable(data.force_classes))) end
  2267 + b.unique = b.name
  2268 + b.randboss = true
  2269 + local boss_id = "RND_BOSS_"..b.name:upper():gsub("[^A-Z]", "_")
  2270 + b.define_as = boss_id
  2271 + b.color = colors.VIOLET
  2272 + b.rank = data.rank or (rng.percent(30) and 4 or 3.5) -- 30% chance of boss rank
  2273 + b.level_range[1] = data.level
  2274 + b.fixed_rating = true
  2275 + if data.life_rating then
  2276 + b.life_rating = data.life_rating(b.life_rating)
  2277 + else
  2278 + b.life_rating = b.life_rating * 1.5 + rng.range(2, 6)
  2279 + end
  2280 + b.max_life = b.max_life or 150
  2281 + b.max_inscriptions = 5 -- Note: This usually won't add inscriptions to NPC bases without them
  2282 +
  2283 + -- Avoid cloning randbosses
  2284 + if b.can_multiply or b.clone_on_hit then
  2285 + b.can_multiply = nil
  2286 + b.clone_on_hit = nil
  2287 + end
  2288 +
  2289 + -- Force resolving some stuff
  2290 + if type(b.max_life) == "table" and b.max_life.__resolver then b.max_life = resolvers.calc[b.max_life.__resolver](b.max_life, b, b, b, "max_life", {}) end
  2291 +
  2292 + -- All bosses have all body parts .. yes snake bosses can use archery and so on ..
  2293 + -- This is to prevent them from having unusable talents
  2294 + b.inven = {}
  2295 + b.body = { INVEN = 1000, QS_MAINHAND = 1, QS_OFFHAND = 1, MAINHAND = 1, OFFHAND = 1, FINGER = 2, NECK = 1, LITE = 1, BODY = 1, HEAD = 1, CLOAK = 1, HANDS = 1, BELT = 1, FEET = 1, TOOL = 1, QUIVER = 1, QS_QUIVER = 1 }
  2296 + b:initBody()
  2297 + -- don't auto equip inventory if forbidden
  2298 + if data.forbid_equip then b.inven[b.INVEN_INVEN]._no_equip_objects = true end
  2299 + b:resolve()
  2300 + -- Start with sustains sustained
  2301 + b[#b+1] = resolvers.sustains_at_birth()
  2302 +
  2303 + -- Leveling stats
  2304 + b.autolevel = "random_boss"
  2305 + b.auto_stats = {}
  2306 +
  2307 + -- Randbosses resemble players so they should use the same resist cap rules
  2308 + -- This is particularly important because at high levels boss ranks get a lot of free resist all
  2309 + b.resists_cap = { all = 70 }
  2310 +
  2311 + -- Update default equipment, if any, to "boss" levels
  2312 + for k, resolver in ipairs(b) do
  2313 + if type(resolver) == "table" and resolver.__resolver == "equip" then
  2314 + resolver[1].id = nil
  2315 + for i, d in ipairs(resolver[1]) do
  2316 + d.name, d.id = nil, nil
  2317 + d.ego_chance = nil
  2318 + d.ignore_material_restriction = true
  2319 + d.forbid_power_source = b.not_power_source
  2320 + d.tome_drops = data.loot_quality or "boss"
  2321 + d.force_drop = (data.drop_equipment == nil) and true or data.drop_equipment
  2322 + end
  2323 + end
  2324 + end
  2325 +
  2326 + -- Boss worthy drops
  2327 + b[#b+1] = resolvers.drops{chance=100, nb=data.loot_quantity or 3, {tome_drops=data.loot_quality or "boss"} }
  2328 + if not data.no_loot_randart then b[#b+1] = resolvers.drop_randart{} end
  2329 +
  2330 + -- On die
  2331 + if data.on_die then
  2332 + b.rng_boss_on_die = b.on_die
  2333 + b.rng_boss_on_die_custom = data.on_die
  2334 + b.on_die = function(self, src)
  2335 + self:check("rng_boss_on_die_custom", src)
  2336 + self:check("rng_boss_on_die", src)
  2337 + end
  2338 + end
  2339 +
  2340 + ------------------------------------------------------------
  2341 + -- Apply talents from classes
  2342 + ------------------------------------------------------------
  2343 + self:applyRandomClass(b, data)
  2344 +
  2345 + b.rnd_boss_on_added_to_level = b.on_added_to_level
  2346 + b.on_added_final = data.rnd_boss_final_adjust
  2347 + b._rndboss_resources_boost = data.resources_boost or 3
  2348 + b._rndboss_talent_cds = data.talent_cds_factor
  2349 + b.on_added_to_level = function(self, ...)
  2350 + self:check("birth_create_alchemist_golem")
  2351 + self:check("rnd_boss_on_added_to_level", ...)
  2352 + self.rnd_boss_on_added_to_level = nil
  2353 + self.on_added_to_level = nil
  2354 +
  2355 + -- Increase talent cds
  2356 + if self._rndboss_talent_cds then
  2357 + local fact = self._rndboss_talent_cds
  2358 + for tid, _ in pairs(self.talents) do
  2359 + local t = self:getTalentFromId(tid)
  2360 + if t.mode ~= "passive" then
  2361 + local bcd = self:getTalentCooldown(t) or 0
  2362 + self.talent_cd_reduction[tid] = (self.talent_cd_reduction[tid] or 0) - math.ceil(bcd * (fact - 1))
  2363 + end
  2364 + end
  2365 + end
  2366 +
  2367 + -- Enhance resource pools (cheat a bit with recovery)
  2368 + for res, res_def in ipairs(self.resources_def) do
  2369 + if res_def.randomboss_enhanced then
  2370 + local capacity
  2371 + if self[res_def.minname] and self[res_def.maxname] then -- expand capacity
  2372 + capacity = (self[res_def.maxname] - self[res_def.minname]) * self._rndboss_resources_boost
  2373 + end
  2374 + if res_def.invert_values then
  2375 + if capacity then self[res_def.minname] = self[res_def.maxname] - capacity end
  2376 + self[res_def.regen_prop] = self[res_def.regen_prop] - (res_def.min and res_def.max and (res_def.max-res_def.min)*.01 or 1) * self._rndboss_resources_boost
  2377 + else
  2378 + if capacity then self[res_def.maxname] = self[res_def.minname] + capacity end
  2379 + self[res_def.regen_prop] = self[res_def.regen_prop] + (res_def.min and res_def.max and (res_def.max-res_def.min)*.01 or 1) * self._rndboss_resources_boost
  2380 + end
  2381 + end
  2382 + end
  2383 + self:resetToFull()
  2384 + end
  2385 +
  2386 + -- Update AI
  2387 + if data.ai then b.ai = data.ai
  2388 + else b.ai = (b.rank > 3) and "tactical" or b.ai
  2389 + end
  2390 + b.ai_state = { talent_in=1, ai_move=data.ai_move or "move_astar" }
  2391 + if data.ai_tactic then
  2392 + b.ai_tactic = data.ai_tactic
  2393 + else
  2394 + b[#b+1] = resolvers.talented_ai_tactic() --calculate ai_tactic table based on talents
  2395 + end
  2396 +
  2397 + -- Anything else
  2398 + if data.post then data.post(b, data) end
  2399 +
  2400 + return b, boss_id
  2401 +end
  2402 +
1916 2403
1917 2404 --- Add one or more character classes to an actor, updating stats, talents, and equipment,
1918 2405 -- Updates autoleveling data so that class skills are advanced with level
... ... @@ -1939,7 +2426,7 @@ end
1939 2426 -- @field data.loot_quality = drop table to use for equipment <"boss">
1940 2427 -- @field data.drop_equipment set true to force dropping of equipment <nil>
1941 2428 -- @param instant set true to force instant learning of talents and generating golem <nil>
1942   -function _M:applyRandomClass(b, data, instant)
  2429 +function _M:applyRandomClassNew(b, data, instant)
1943 2430 if not data.level then data.level = b.level end -- use the level specified if needed
1944 2431
1945 2432 ------------------------------------------------------------
... ... @@ -2084,7 +2571,7 @@ end
2084 2571
2085 2572 --- Creates a random Boss (or elite) actor
2086 2573 -- @param base = base actor to add classes/talents to
2087   --- calls _M:applyRandomClass(b, data, instant) to add classes, talents, and equipment based on class descriptors
  2574 +-- calls _M:applyRandomClassNew(b, data, instant) to add classes, talents, and equipment based on class descriptors
2088 2575 -- handles data.nb_classes, data.force_classes, data.class_filter, ...
2089 2576 -- optional parameters:
2090 2577 -- @field data.init = function(data, b) to run before generation
... ... @@ -2100,7 +2587,7 @@ end
2100 2587 -- @field data.on_die set true to run base.rng_boss_on_die and base.rng_boss_on_die_custom on death <nil>
2101 2588 -- @field data.name_scheme <randart_name_rules.default>
2102 2589 -- @field data.post = function(b, data) to run last to finish generation
2103   -function _M:createRandomBoss(base, data)
  2590 +function _M:createRandomBossNew(base, data)
2104 2591 local b = base:clone()
2105 2592 data = data or {level=1}
2106 2593 if data.init then data.init(data, b) end
... ... @@ -2198,7 +2685,7 @@ function _M:createRandomBoss(base, data)
2198 2685 -- Apply talents from classes
2199 2686 ------------------------------------------------------------
2200 2687 -- apply classes (instant to level up stats/talents before equipment is resolved)
2201   - self:applyRandomClass(b, data, true)
  2688 + self:applyRandomClassNew(b, data, true)
2202 2689
2203 2690 b.rnd_boss_on_added_to_level = b.on_added_to_level
2204 2691 b._rndboss_resources_boost = data.resources_boost or 3
... ...
... ... @@ -476,20 +476,20 @@ end
476 476 -- Triggered after the entity is resolved
477 477 function _M:addedToLevel(level, x, y)
478 478 if not self:attr("difficulty_boosted") and not game.party:hasMember(self) and not (self.summoner or self.summoned) then
479   - -- make adjustments for game difficulty to talent levels, max life, add classes to bosses
480   - local talent_mult, life_mult, nb_classes = 1, 1, 0
  479 + -- make adjustments for game difficulty to talent levels, max life, and bonus fixedboss classes
  480 + local talent_mult, life_mult, class_mult = 1,1,1
481 481 if game.difficulty == game.DIFFICULTY_NIGHTMARE then
482 482 talent_mult = 1.3
483   - life_mult = 1.5
484   - nb_classes = util.bound(self.rank - 3.5, 0, 1) -- up to 1 extra class
  483 + class_mult = 1.3
  484 + life_mult = 1.1
485 485 elseif game.difficulty == game.DIFFICULTY_INSANE then
486 486 talent_mult = 1.8
487   - life_mult = 2.0
488   - nb_classes = util.bound(self.rank - 3, 0, 2) -- up to 2 extra classes
  487 + class_mult = 1.8
  488 + life_mult = 1.2
489 489 elseif game.difficulty == game.DIFFICULTY_MADNESS then
490 490 talent_mult = 2.7
  491 + class_mult = 2.7
491 492 life_mult = 3.0
492   - nb_classes = util.bound((self.rank - 3)*1.5, 0, 3) -- up to 3 extra classes
493 493 end
494 494 if talent_mult ~= 1 then
495 495 -- increase level of innate talents
... ... @@ -499,13 +499,22 @@ function _M:addedToLevel(level, x, y)
499 499 self:learnTalent(tid, true, math.floor(lev*(talent_mult - 1)))
500 500 end
501 501 end
502   - -- add the extra character classes (halved for randbosses)
503   - if nb_classes > 0 and not self.no_difficulty_random_class then
504   - -- Note: talent levels from added classes are not adjusted for difficulty directly
505   - -- This means that the NPC's innate talents are generally higher level, preserving its "character"
506   - if self.randboss then nb_classes = nb_classes/2 end
507   - local data = {auto_sustain=true, forbid_equip=nb_classes<1, nb_classes=nb_classes, update_body=true, spend_points=true, autolevel=nb_classes<2 and self.autolevel or "random_boss"}
508   - game.state:applyRandomClass(self, data, true)
  502 +
  503 + -- Resolve bonus classes for fixed bosses
  504 + -- Note: talent levels from added classes are not adjusted for difficulty directly
  505 + -- This means that the NPC's innate talents are generally higher level, preserving its "character"
  506 + -- Fixedboss random classes start at level 14 to avoid breaking early game balance
  507 + -- For now if not defined the starting level of fixedboss classes is 80% of their actor level, unsure what this value should be
  508 + if self.rank >= 3.5 and not self.randboss and not self.no_difficulty_random_class then
  509 + if self.auto_classes then
  510 + for _, class in pairs(self.auto_classes) do
  511 + class.level_rate = class.level_rate * class_mult
  512 + end
  513 + else
  514 + local data = {auto_sustain=true, forbid_equip=false, start_level = math.max(14, self.level * 0.8), nb_classes=1, level_rate = class_mult*100, update_body=true, spend_points=true, autolevel="random_boss"}
  515 + game.state:applyRandomClassNew(self, data, true)
  516 + end
  517 +
509 518 self[#self+1] = resolvers.talented_ai_tactic("instant") -- regenerate AI TACTICS with the new class(es)
510 519 self:resolve() self:resolve(nil, true)
511 520 self:resetToFull()
... ...
... ... @@ -527,6 +527,10 @@ newEntity{ base = "BASE_NPC_HORROR",
527 527 life_regen = 0.25,
528 528 combat_armor = 12, combat_def = 24,
529 529
  530 + rnd_boss_init = function(self, data)
  531 + self.combat_physspeed = math.max(1, self.combat_physspeed - 2) -- A bit more sanity when randbossed
  532 + end,
  533 +
530 534 ai = "tactical", ai_state = { ai_move="move_complex", talent_in=2, ally_compassion=0 },
531 535
532 536 on_melee_hit = {[DamageType.PHYSICALBLEED]=resolvers.mbonus(14, 2)},
... ...
... ... @@ -36,6 +36,9 @@ newEntity{
36 36 ai = "dumb_talented_simple", ai_state = { ai_move="move_complex", talent_in=1, },
37 37 stats = { str=1, dex=20, mag=3 },
38 38 global_speed_base = 2,
  39 + rnd_boss_init = function(self, data)
  40 + self.inc_damage.all = (self.inc_damage.all or 0) - 30 -- Compensate for high global speed
  41 + end,
39 42 infravision = 10,
40 43 combat_armor = 1, combat_def = 10,
41 44 rank = 1,
... ...
... ... @@ -137,6 +137,9 @@ newTalent{
137 137 name = "Kinetic Shield",
138 138 type = {"psionic/absorption", 1},
139 139 require = psi_cun_req1,
  140 + rnd_boss_restrict = function(self, t, data) -- Flat damage reduction can be obnoxious early game
  141 + return data.level < 15
  142 + end,
140 143 mode = "sustained", no_sustain_autoreset = true,
141 144 points = 5,
142 145 sustain_psi = 10,
... ... @@ -197,6 +200,9 @@ newTalent{
197 200 name = "Thermal Shield",
198 201 type = {"psionic/absorption", 1},
199 202 require = psi_cun_req2,
  203 + rnd_boss_restrict = function(self, t, data) -- Flat damage reduction can be obnoxious early game
  204 + return data.level < 15
  205 + end,
200 206 mode = "sustained", no_sustain_autoreset = true,
201 207 points = 5,
202 208 sustain_psi = 10,
... ... @@ -259,6 +265,9 @@ newTalent{
259 265 name = "Charged Shield",
260 266 type = {"psionic/absorption", 1},
261 267 require = psi_cun_req3,
  268 + rnd_boss_restrict = function(self, t, data) -- Flat damage reduction can be obnoxious early game
  269 + return data.level < 15
  270 + end,
262 271 mode = "sustained", no_sustain_autoreset = true,
263 272 points = 5,
264 273 sustain_psi = 10,
... ...
... ... @@ -59,6 +59,9 @@ newTalent{
59 59 mana = 60,
60 60 cooldown = 25,
61 61 tactical = { ATTACK = { ARCANE = 3 }, DISABLE = 2 },
  62 + rnd_boss_restrict = function(self, t)
  63 + return self.level < 15
  64 + end,
62 65 range = 7,
63 66 requires_target = true,
64 67 getMax = function(self, t) return 200 + self:combatTalentSpellDamage(t, 28, 850) end,
... ...
... ... @@ -88,6 +88,16 @@ newEntity{ base="BASE_NPC_CANINE", define_as = "WITHERING_THING",
88 88 autolevel = "warriorwill",
89 89 ai = "tactical", ai_state = { talent_in=2 },
90 90 ai_tactic = resolvers.tactic"melee",
  91 +
  92 + resolvers.auto_equip_filters("Doomed"),
  93 + auto_classes={{class="Doomed", start_level=12, level_rate=75}},
  94 +
  95 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  96 + on_added_to_level = function(self)
  97 + if self.level <= 16 then
  98 + self.ai_tactic.escape = 0
  99 + end
  100 + end,
91 101
92 102 on_die = function(self, who)
93 103 game.player:resolveSource():setQuestStatus("start-thaloren", engine.Quest.COMPLETED, "heart-gloom")
... ... @@ -130,6 +140,9 @@ newEntity{ define_as = "DREAMING_ONE",
130 140 autolevel = "wildcaster",
131 141 ai = "tactical", ai_state = { talent_in=1 },
132 142
  143 + resolvers.auto_equip_filters("Solipsist"),
  144 + auto_classes={{class="Solipsist", start_level=12, level_rate=75}},
  145 +
133 146 on_die = function(self, who)
134 147 game.player:resolveSource():setQuestStatus("start-thaloren", engine.Quest.COMPLETED, "heart-gloom")
135 148 game.player:resolveSource():setQuestStatus("start-thaloren", engine.Quest.COMPLETED, "heart-gloom-purified")
... ...
... ... @@ -63,6 +63,16 @@ newEntity{ base = "BASE_NPC_YAECH", define_as = "MURGOL",
63 63
64 64 autolevel = "wildcaster",
65 65 ai = "tactical", ai_state = { talent_in=2, },
  66 +
  67 + resolvers.auto_equip_filters("Mindslayer"),
  68 + auto_classes={{class="Mindslayer", start_level=12, level_rate=75}},
  69 +
  70 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  71 + on_added_to_level = function(self)
  72 + if self.level <= 16 then
  73 + self.ai_tactic.escape = 0
  74 + end
  75 + end,
66 76
67 77 on_die = function(self, who)
68 78 game.player:setQuestStatus("start-yeek", engine.Quest.COMPLETED, "murgol")
... ... @@ -189,6 +199,16 @@ newEntity{ base="BASE_NPC_NAGA", define_as = "NASHVA",
189 199 ai = "tactical", ai_state = { talent_in=2, ai_move="move_astar", },
190 200 ai_tactic = resolvers.tactic"melee",
191 201
  202 + resolvers.auto_equip_filters("Mindslayer"),
  203 + auto_classes={{class="Mindslayer", start_level=12, level_rate=75}},
  204 +
  205 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  206 + on_added_to_level = function(self)
  207 + if self.level <= 16 then
  208 + self.ai_tactic.escape = 0
  209 + end
  210 + end,
  211 +
192 212 on_die = function(self, who)
193 213 game.player:setQuestStatus("start-yeek", engine.Quest.COMPLETED, "murgol")
194 214 game.player:setQuestStatus("start-yeek", engine.Quest.COMPLETED, "murgol-invaded")
... ...
... ... @@ -125,6 +125,17 @@ newEntity{ base="BASE_NPC_BEAR", define_as = "NORGOS",
125 125 autolevel = "warrior",
126 126 ai = "tactical", ai_state = { talent_in=2, ai_move="move_astar", },
127 127 ai_tactic = resolvers.tactic"melee",
  128 +
  129 + resolvers.auto_equip_filters("Brawler"),
  130 + auto_classes={{class="Brawler", start_level=12, level_rate=75}},
  131 +
  132 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  133 + on_added_to_level = function(self)
  134 + if self.level <= 16 then
  135 + self.ai_tactic.escape = 0
  136 + end
  137 + end,
  138 +
128 139 resolvers.inscriptions(1, "infusion"),
129 140
130 141 on_die = function(self, who)
... ...
... ... @@ -63,6 +63,15 @@ newEntity{ define_as = "INQUISITOR",
63 63 autolevel = "warriormage",
64 64 ai = "tactical", ai_state = { talent_in=2, ai_move="move_astar", },
65 65
  66 + auto_classes={{class="Corruptor", start_level=12, level_rate=75}},
  67 +
  68 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  69 + on_added_to_level = function(self)
  70 + if self.level <= 16 then
  71 + self.ai_tactic.escape = 0
  72 + end
  73 + end,
  74 +
66 75 on_die = function(self, who)
67 76 game.player:resolveSource():setQuestStatus("start-shaloren", engine.Quest.COMPLETED, "rhaloren")
68 77 end,
... ...
... ... @@ -141,6 +141,15 @@ newEntity{ base = "BASE_NPC_RITCH_REL", define_as = "HIVE_MOTHER",
141 141 autolevel = "dexmage",
142 142 ai = "tactical", ai_state = { talent_in=2, },
143 143
  144 + auto_classes={{class="Summoner", start_level=12, level_rate=75},},
  145 +
  146 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  147 + on_added_to_level = function(self)
  148 + if self.level <= 16 then
  149 + self.ai_tactic.escape = 0
  150 + end
  151 + end,
  152 +
144 153 on_die = function(self, who)
145 154 game.player:setQuestStatus("start-yeek", engine.Quest.COMPLETED, "ritch")
146 155 end,
... ...
... ... @@ -77,6 +77,15 @@ newEntity{ define_as = "SHADE",
77 77 auto_classes={{class="Archmage", start_level=12, level_rate=75}},
78 78 ai = "tactical", ai_state = { talent_in=3, ai_move="move_astar", },
79 79
  80 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  81 + -- In this case safe_range being set while talent_in is above 1 still results in a lot of kiting, so we lower that too
  82 + on_added_to_level = function(self)
  83 + if self.level <= 16 then
  84 + self.ai_tactic.safe_range = 1
  85 + self.ai_tactic.escape = 0
  86 + end
  87 + end,
  88 +
80 89 on_die = function(self, who)
81 90 game.state:activateBackupGuardian("KOR_FURY", 3, 35, ".. yes I tell you! The old ruins of Kor'Pul are still haunted!")
82 91 game.player:resolveSource():setQuestStatus("start-allied", engine.Quest.COMPLETED, "kor-pul")
... ... @@ -118,6 +127,14 @@ newEntity{ base = "BASE_NPC_THIEF", define_as = "THE_POSSESSED",
118 127 auto_classes={{class="Arcane Blade", start_level=12, level_rate=75}},
119 128 ai = "tactical", ai_state = { talent_in=2, ai_move="move_astar", },
120 129
  130 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  131 + on_added_to_level = function(self)
  132 + if self.level <= 16 then
  133 + self.ai_tactic.safe_range = 1
  134 + self.ai_tactic.escape = 0
  135 + end
  136 + end,
  137 +
121 138 on_die = function(self, who)
122 139 game.state:activateBackupGuardian("KOR_FURY", 3, 35, ".. yes I tell you! The old ruins of Kor'Pul are still haunted!")
123 140 game.player:resolveSource():setQuestStatus("start-allied", engine.Quest.COMPLETED, "kor-pul")
... ...
... ... @@ -60,6 +60,8 @@ newEntity{ base="BASE_NPC_CRYSTAL", define_as = "SPELLBLAZE_CRYSTAL",
60 60 autolevel = "caster",
61 61 ai = "tactical", ai_state = { talent_in=1, ai_move="move_astar", },
62 62 ai_tactic = resolvers.tactic"ranged",
  63 +
  64 + auto_classes={{class="Archmage", start_level=12, level_rate=75}},
63 65
64 66 on_die = function(self, who)
65 67 game.player:resolveSource():setQuestStatus("start-shaloren", engine.Quest.COMPLETED, "spellblaze")
... ...
... ... @@ -75,10 +75,17 @@ newEntity{ define_as = "TROLL_PROX",
75 75 inc_damage = { all = -40 },
76 76
77 77 autolevel = "warrior",
78   - auto_classes={{class="Berserker", start_level=11, level_rate=75},},
  78 + auto_classes={{class="Berserker", start_level=12, level_rate=75},},
79 79 ai = "tactical", ai_state = { talent_in=3, ai_move="move_astar", },
80 80 ai_tactic = resolvers.tactic"melee",
81 81
  82 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  83 + on_added_to_level = function(self)
  84 + if self.level <= 16 then
  85 + self.ai_tactic.escape = 0
  86 + end
  87 + end,
  88 +
82 89 -- Drop the note when near death (but before death, so that Kill bill achievement is possible)
83 90 on_takehit = function(self, val)
84 91 if self.life - val < self.max_life * 0.4 then
... ... @@ -138,10 +145,17 @@ newEntity{ define_as = "TROLL_SHAX",
138 145 inc_damage = { all = -40 },
139 146
140 147 autolevel = "warrior",
141   - auto_classes={{class="Berserker", start_level=11, level_rate=75},},
  148 + auto_classes={{class="Berserker", start_level=12, level_rate=75},},
142 149 ai = "tactical", ai_state = { talent_in=3, ai_move="move_astar", },
143 150 ai_tactic = resolvers.tactic"melee",
144 151
  152 + -- Override the recalculated AI tactics to avoid problematic kiting in the early game
  153 + on_added_to_level = function(self)
  154 + if self.level <= 16 then
  155 + self.ai_tactic.escape = 0
  156 + end
  157 + end,
  158 +
145 159 -- Drop the note when near death (but before death, so that Kill bill achievement is possible)
146 160 on_takehit = function(self, val)
147 161 if self.life - val < self.max_life * 0.4 then
... ... @@ -202,7 +216,7 @@ This is the troll the notes spoke about, no doubt.]],
202 216 resolvers.inscriptions(1, {"wild infusion", "heroism infusion"}),
203 217
204 218 autolevel = "warrior",
205   - auto_classes={{class="Berserker", start_level=11, level_rate=75},},
  219 + auto_classes={{class="Berserker", start_level=10, level_rate=100},},
206 220 ai = "tactical", ai_state = { talent_in=3, ai_move="move_astar", },
207 221 ai_tactic = resolvers.tactic"melee",
208 222
... ...