From ae3ae6105d70009855cb45d1e756c5b43229cea0 Mon Sep 17 00:00:00 2001
From: Hachem_Muche <Hachem_Muche@stanfordalumni.org>
Date: Sat, 31 Mar 2018 14:24:15 -0700
Subject: [PATCH] NPC Class Autoleveling

Updated the way NPCs (randbosses) are assigned character classes (game.state:applyRandomClass).
The new method makes high level NPCs with character classes scale like player levels and tends to increase the number of talents randomly learned while limiting the maximum talent levels.
This is a nerf for high level randbosses (including those enhanced due to game difficulty) and a buff for fixed bosses that are updated, while making turning their difficulty much simpler.
This replaces the previous method of assigning an increasing number of class talents at maximum level, that could result in boss's skills scaling much faster than its actor level.

Talents are learned randomly one level at a time and are restricted by the normal progression of talent and stat points and all normal talent restrictions.
After setup, NPCs automatically advance in their character classes when gaining levels, tending to focus their talent choices with randomly determined preferred categories.

Random bosses and bosses enhanced for game difficulty can use "partial" character classes.  Game balance tuning is handled my adjusting the number of added classes.
Adjusted the number of character classes assigned at higher difficulties:
NIGHTMARE -- max 1 random class (BUFF), INSANE -- max 2 random classes , MADNESS -- max 3 random classes

NPC (fixed boss) definitions can be updated to specify automatic class advancement begining at a certain level and rate.
This allows many bosses that become unchallenging on advanced difficulties or when spawning in the I.D. to be easily buffed by specifying character class advancement based on level.

Summary of function changes:

Actor:learnStats(statorder, repeats)
Updated to maintain the current stat index within the passed statorder table.
This allows multiple stat progressions to be applied to the same Actor and repeated multiple times with the same function call.

Actor:levelupClass(c_data)
New function to automatically assign character class levels to an actor according to c_data.
Sets up the self.auto_classes table, retrieves data as needed from class birth descriptors.
Assigns stat/talent/category points randomly on a level-by-level basis.

Actor:resolveLevelTalents()
Updated to process any self.auto_classes defined

game.state:applyRandomClass(b, data, instant)
Works as before, but is updated to use Actor:levelupClass to set up class definitions.
nb_classes can be a flotaing point number, allowing "fractional" classes to be assigned: (1.5 == one class at 100% actor level plus another class at 50% of actor level).
The instant parameter forces advancement in the class(es) immediately.
Fixed a bug where auto_equip_filters were being copied even when forbid_equip was set.

game.state:createRandomBoss(base, data)
By default, randombosses are nerfed slightly, getting 1.75 random classes by default.

NPC:addedToLevel(level, x, y)
Higher game difficulties assign fewer randomclasses based on rank, which may be partial classes:
NIGHTMARE -- up to 1 random class, INSANE -- up to 2 random classes, MADNESS -- up to 3 random classes
Random bosses gain extra character classes at 1/2 the rate of fixed bosses.

resolvers.sustains_at_birth()
Will check that a talent is inactive before trying to activate it.

resolvers.talented_ai_tactic(method, tactic_emphasis, weight_power)
May specify "instant" as the method to force recalculating the talent table immediately when resolved (instead of through on_added_to_level).

RandomActor debug Dialog: updated Help reference to show header for Actor:levelupClass
SummonCreature debug Dialog: updated to spawn the alchemy golem if needed
---
 game/modules/tome/class/Actor.lua             | 412 +++++++++++++++++-
 game/modules/tome/class/GameState.lua         | 258 +++++------
 game/modules/tome/class/NPC.lua               |  28 +-
 .../tome/dialogs/debug/RandomActor.lua        |  13 +-
 .../tome/dialogs/debug/SummonCreature.lua     |   2 +
 game/modules/tome/resolvers.lua               |  26 +-
 6 files changed, 531 insertions(+), 208 deletions(-)

diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index 8f35a12bb3..626fe4893a 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -3295,25 +3295,393 @@ function _M:die(src, death_note)
 	return true
 end
 
-function _M:learnStats(statorder)
-	self.auto_stat_cnt = self.auto_stat_cnt or 1
+--- Learn stats (stat increment) in a specified order up to a maximum
+-- @param[table] statorder: an orded list of stat ids in which to learn, generally all learned in one character level
+-- @param repeats: maximum number of times to apply the statorder <1>
+function _M:learnStats(statorder, repeats)
+	statorder.idx = statorder.idx or 1
+	repeats = (repeats or 1)*#statorder
 	local nb = 0
 	local max = 60
 
-	-- Allow to go over a natural 60, up to 80 at level 50
+	-- Allow stats to go over a natural 60, up to 80 at level 50
 	if not self.no_auto_high_stats then max = 60 + (self.level * 20 / 50) end
 
-	while self.unused_stats > 0 do
-		if self:getStat(statorder[self.auto_stat_cnt]) < max then
-			self:incIncStat(statorder[self.auto_stat_cnt], 1)
+	while self.unused_stats > 0 and nb < repeats do
+		if self:getStat(statorder[statorder.idx]) < max then
+			self:incIncStat(statorder[statorder.idx], 1)
 			self.unused_stats = self.unused_stats - 1
 		end
-		self.auto_stat_cnt = util.boundWrap(self.auto_stat_cnt + 1, 1, #statorder)
+		statorder.idx = util.boundWrap(statorder.idx + 1, 1, #statorder)
 		nb = nb + 1
-		if nb >= #statorder then break end
 	end
 end
 
+--- Actor learns/advances in a character class, randomly gaining stats and learning talents based on levels in the class
+--	When first called, the actor gains starting stat bonuses and talents and learns talent categories according to the birth class descriptor
+-- Note: this does not handle any special class requirements, such as creating a golem, generating equipment, etc.
+-- @see game.state:applyRandomClass
+-- @param c_data[table] contains info on the class to learn/levelup in:
+-- @field class: name (birth descriptor subclass name) of the character class to learn ("Bulwark", "Solipsist", ...)
+-- @field start_level: starting actor level to begin leveling in the class <1>
+-- @field level_rate: rate levels in character class are gained as % of actor level <100>
+-- @field check_talents_level: set true to enforce character level limits on talent levels (i.e. level 5 at level 50) <nil>
+-- @field level_by_class: set true to use class level rather than actor level when checking talent/stat limits <nil>
+-- @foe;d ignore_special: set true to skip checking talent special requirements
+-- @field auto_sustain: set true to automatically turn on sustained talents learned <nil>
+-- @field tt_focus: talent type focus, higher values cause talent selections to be focused within fewer talent types <3>
+-- @field use_actor_points: set true to apply stat/talent points from base actor levels in addtion to class levels <nil>
+--  The following fields are automatically generated or updated from the class birth descriptor:
+-- @field ttypes[table]: talent trees to learn talents from (updated with birth descriptor)
+--		{talent_type_name = {[1]=known, [2]=mastery_add}, ...}
+-- @field auto_stats: ordered list of stat ids to use when applying unused_stats points
+--		generated from class descriptor by default, set false to disable, see Actor:learnStats
+-- Additional talent inputs for each talent definition t:
+--	t.random_boss_rarity: if defined, the percent chance the talent may be learned each time randomly selected
+function _M:levelupClass(c_data)
+	c_data.last_level = c_data.last_level or 0
+	c_data.start_level = c_data.start_level or 1
+
+	local new_level = math.ceil((self.level - c_data.start_level + 1)*(c_data.level_rate or 100)/100)
+	
+	if new_level <= c_data.last_level then return end
+	print("[Actor:levelupClass]", self.name, "auto level up", c_data.class, c_data.last_level, "-->", new_level, c_data)
+	
+table.set(game, "debug", "levelupClass", {act=self, c_data=c_data}) -- debugging
+
+	local ttypes
+	-- temporarily remove any previous stat/talent points if they won't be used
+	local base_points = {self.unused_stats, self.unused_talents, self.unused_generics}
+	if not c_data.use_actor_points then
+		self.unused_stats, self.unused_talents, self.unused_generics = 0, 0, 0 
+	end
+
+	-- Initialize if needed, updating auto_classes table
+	if c_data.class and not c_data.initialized then -- build talent category list from the class
+		local Birther = require "engine.Birther"
+		local c_def = Birther.birth_descriptor_def.subclass[c_data.class]
+		if not c_def then
+			print("[Actor:levelupClass] ### undefined class:", c_data.class)
+			return
+		end
+
+		local mclasses = Birther.birth_descriptor_def.class
+		local mclass = nil
+		for name, data in pairs(mclasses) do
+			if data.descriptor_choices and data.descriptor_choices.subclass and data.descriptor_choices.subclass[c_def.name] then mclass = data break end
+		end
+		if not mclass then
+			print("[Actor:levelupClass] ###class", c_data.class, "has no parent class type###")
+			return 
+		end
+
+		print(("[Actor:levelupClass] %s %s ## Initialzing auto_class %s (%s) %s%% level_rate from level %s ##"):format(self.uid, self.name, c_data.class, mclass.name, c_data.level_rate, c_data.start_level))
+
+		-- update class descriptor list and build inherent power sources
+		self.descriptor = self.descriptor or {}
+		self.descriptor.classes = self.descriptor.classes or {}
+		table.append(self.descriptor.classes, {c_def.name})
+		
+		-- build inherent power sources and forbidden power sources
+		-- self.forbid_power_source --> self.not_power_source used for classes
+		self.power_source = table.merge(self.power_source or {}, c_def.power_source or {})
+		self.not_power_source = table.merge(self.not_power_source or {}, c_def.not_power_source or {})
+		-- update power source parameters with the new class
+		self.not_power_source, self.power_source = game.state:updatePowers(game.state:attrPowers(self, self.not_power_source), self.power_source)
+		print("  *** power types: not_power_source =", table.concat(table.keys(self.not_power_source),","), "power_source =", table.concat(table.keys(self.power_source),","))
+		
+		-- apply class stat bonuses and set up class auto_stats (in auto_classes)
+		local auto_stats
+		if c_def.stats and c_data.auto_stats ~= false then
+			auto_stats = {}
+			for stat, v in pairs(c_def.stats or {}) do
+				local stat_id = self.stats_def[stat].id
+				self.stats[stat_id] = (self.stats[stat_id] or 10) + v
+				for i = 1, v do auto_stats[#auto_stats+1] = stat_id end
+			end
+			c_data.auto_stats = auto_stats
+		end
+		c_data.auto_stats = c_data.auto_stats or auto_stats
+
+		ttypes = {}
+		for tt, d in pairs(mclass.talents_types or {}) do
+			self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
+			ttypes[tt] = table.clone(d)
+		end
+		for tt, d in pairs(mclass.unlockable_talents_types or {}) do
+			self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
+			ttypes[tt] = table.clone(d)
+		end
+		for tt, d in pairs(c_def.talents_types or {}) do
+			self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
+			ttypes[tt] = table.clone(d)
+		end
+		for tt, d in pairs(c_def.unlockable_talents_types or {}) do
+			self:learnTalentType(tt, d[1]) self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
+			ttypes[tt] = table.clone(d)
+		end
+		
+		-- set up input talent categories specified (generally for non-class talents)
+		if c_data.ttypes then
+			for tt, d in pairs(c_data.ttypes) do
+				if not self:knowTalentType(tt) then
+					if type(d) ~= "table" then
+						d={true, type(d) == "number" and d or rng.range(1, 3)*0.1}
+					else d=table.clone(d)
+					end
+					self:learnTalentType(tt, d[1])
+					self:setTalentTypeMastery(tt, (self:getTalentTypeMastery(tt) or 1) + d[2])
+					ttypes[tt] = table.mergeAdd(ttypes[tt] or {}, d)
+				end
+			end
+		end
+		if not next(ttypes) then -- if not specified, use all known talent types
+			for tt, known in pairs(self.talents_types) do
+				ttypes[tt] = {known, (self:getTalentTypeMastery(tt) or 1) - 1}
+			end
+		end
+		
+		-- Note: could limit number of talent trees selected here to limit # talents learned
+		
+--print("\t *** auto_levelup initialized talent category choices:", mclass.name , c_def.name , "\n\t")
+		-- set up (semi-random) rarity levels for talent categories
+		-- This tends to focus learned talents within certain trees (usually those with improved mastery)
+		local tt_count = 0
+		local unknown_tt={}
+		local tt_focus = c_data.tt_focus or 3
+		for tt, d in pairs(ttypes) do
+			d.tt = tt
+			tt_count = tt_count + 1
+			d.tt_count = tt_count
+			d.rarity = d.rarity or (1.3 + 0.5*tt_count)/math.max(0.1, self:getTalentTypeMastery(tt)*rng.float(0.1, tt_focus))^2
+--print(("\t *** %-40s  rarity %5.3f"):format(tt, d.rarity))
+			if not d[1] then table.insert(unknown_tt, d) end
+		end
+		c_data.unknown_tt = unknown_tt
+		c_data.ttypes = ttypes
+		
+		-- Assign class starting talents and set them to level up later
+		for tid, v in pairs(c_def.talents or {}) do
+			c_data.auto_talents = c_data.auto_talents or {}
+			local t = self:getTalentFromId(tid)
+			if not t.no_npc_use and (not t.random_boss_rarity or rng.chance(t.random_boss_rarity)) then
+				local every = 0
+				if t.points > 1 then
+					every = math.ceil(50/(t.points * 1.2))
+					table.insert(c_data.auto_talents, {tid=tid, start_level=c_data.start_level, base=v, every=every})
+				end
+				print(("\t ** learning %s birth talent %s %s (every %s levels)"):format(c_data.class, tid, v, every))
+				self:learnTalent(tid, true, v)
+			end
+		end
+		c_data.initialized = true
+	end
+
+	local init_level, learn_tids, learn_stats = c_data.last_level -- for log output summary
+	local to_activate = {}
+	while new_level > c_data.last_level do
+		c_data.last_level = c_data.last_level + 1
+		--print("{Actor:levelupClass] to", c_data.class, c_data.last_level)
+		learn_tids = learn_tids or {}
+		learn_stats = learn_stats or {}
+		-- Gain talent and category points, similar to Actor:levelup: (but with no prodigies)
+		if not self.no_points_on_levelup then
+			self.unused_stats = self.unused_stats + (self.stats_per_level or 3) + self:getRankStatAdjust()
+			self.unused_talents = self.unused_talents + 1
+			self.unused_generics = self.unused_generics + 1
+			if c_data.last_level % 5 == 0 then self.unused_talents = self.unused_talents + 1 end
+			if c_data.last_level % 5 == 0 then self.unused_generics = self.unused_generics - 1 end
+
+			if self.extra_talent_point_every and c_data.last_level % self.extra_talent_point_every == 0 then self.unused_talents = self.unused_talents + 1 end
+			if self.extra_generic_point_every and c_data.last_level % self.extra_generic_point_every == 0 then self.unused_generics = self.unused_generics + 1 end
+
+			-- At levels 10, 20 and 36 and then every 30 levels, we gain a new talent type
+			if c_data.last_level == 10 or c_data.last_level == 20 or c_data.last_level == 36 or (c_data.last_level > 50 and (c_data.last_level - 6) % 30 == 0) then
+				self.unused_talents_types = self.unused_talents_types + 1
+			end
+			-- if c_data.last_level == 30 or c_data.last_level == 42 then self.unused_prodigies = self.unused_prodigies + 1 end
+		elseif type(self.no_points_on_levelup) == "function" then
+			self:no_points_on_levelup()
+		end
+
+		--print((" *** level: %s/%s stats: %s talents: %s generics: %s categories: %s prodigies: %s"):format(c_data.last_level, new_level, self.unused_stats, self.unused_talents, self.unused_generics, self.unused_talents_types, self.unused_prodigies))
+
+		-- automatically level up any auto_talents, (usualy birth talents)
+		if c_data.auto_talents then
+			for i, d in ipairs(c_data.auto_talents) do
+				if c_data.last_level > d.start_level and (c_data.last_level - d.start_level)%d.every == 0 then
+					--print(("\t ** advancing %s auto_talent %s"):format(c_data.class, d.tid))
+					self:learnTalent(d.tid, true)
+				end
+			end
+		end
+
+		ttypes = c_data.ttypes
+
+		-- generate list of possible talent types based on the master list
+		--print("\t *** auto_levelup available talent categories:\n\t", table.concat(table.keys(ttypes), ", "))
+		local tt_choices = {}
+		for tt, d in pairs(ttypes) do
+			table.insert(tt_choices, d)
+		end
+
+		local fails, max_fails = 0, #tt_choices*4
+		-- assign unused category points
+		while self.unused_talents_types >= 1 and fails <= 5 do
+			print("\t *** Assigning", self.unused_talents_types, "category points")
+
+			-- 50% chance to learn a new talent category if possible
+			local tt
+			if rng.percent(50) then	tt = rng.tableRemove(c_data.unknown_tt) end
+			if not tt then tt = rng.table(tt_choices) end
+			
+			if tt then
+				if self:knowTalentType(tt.tt) then
+					print("\t *** auto_levelup IMPROVING TALENT TYPE", tt.tt)
+					local ml = self:getTalentTypeMastery(tt.tt) or 1
+					self:setTalentTypeMastery(tt.tt, ml + (ml <= 1 and 0.2 or 0.1)) -- 0.2 for 1st then 0.1 thereafter
+				else
+					print("\t *** auto_levelup LEARNING TALENT TYPE", tt.tt)
+					self:learnTalentType(tt.tt, true)
+					tt.rarity = tt.rarity/2  -- makes talents within an unlocked talent tree more likely to be learned
+				end
+				--print("\t *** talent type mastery:", tt, self:getTalentTypeMastery(tt))
+				self.unused_talents_types = self.unused_talents_types - 1
+			else fails = fails + 1
+			end
+		end
+		
+		-- learn talents randomly from available talent trees
+		-- Note: could put limit on number of talents learned here (similar to previous method)
+		fails = 0
+		local tt, tt_idx, tt_def
+		local t, t_idx
+		while fails < max_fails and (self.unused_talents >= 1 or self.unused_generics >= 1) do
+			-- pick a talent tree
+			tt, tt_idx = rng.rarityTable(tt_choices)
+			if not tt then break end
+			tt = tt.tt
+			tt_def = tt and self:knowTalentType(tt) and self.talents_types_def[tt]
+			-- begin category loop
+			if tt_def and (tt_def.generic and self.unused_generics >= 1 or not tt_def.generic and self.unused_talents >=1) then
+				--print("\t *** checking category:", tt)
+				-- try to find a talent to learn
+				local t_choices = {}
+				local nb_known = self:numberKnownTalent(tt)
+				-- update talent choices with each talent in the tree that can be learned
+				for i, t in ipairs(tt_def.talents) do
+					if t.no_npc_use or t.not_on_random_boss then
+						nb_known = nb_known + 1 -- treat as known to allow later talents to be learned
+					elseif t.type[2] and nb_known >= t.type[2] - 1 and (not t.random_boss_rarity or rng.percent(t.random_boss_rarity)) then -- check category talents known
+						table.insert(t_choices, t)
+					end
+				end
+				--	print("talent choices for", tt)	for i, t in ipairs(t_choices) do print("\t", i, t.id, t.name) end
+				local ok
+				repeat
+					t, t_idx = rng.table(t_choices)
+					if not t then break end
+					ok = true
+					local tlev = self:getTalentLevelRaw(t) + 1 -- talent level to learn
+					-- check max talent level
+					local max = t.points
+					if max > 1 then
+						max = math.max(max, math.ceil(max*1.2*c_data.last_level/50))
+						if c_data.check_talents_level then -- apply character level limits
+							max = math.min(max, t.points + math.max(0, math.floor((c_data.last_level - 50) / 10)) + (self.talents_inc_cap and self.talents_inc_cap[t.id] or 0))
+						end
+					end
+					if tlev > max then
+						ok = false
+					end
+					
+					if ok and t.require then -- check requirements to learn the talent
+						local req = rawget(t, "require")
+						if type(req) == "function" then req = req(self, t) end
+						if req then
+							if req.level then -- check talent-defined level limits
+								local lev = util.getval(req.level, tlev)
+								if c_data.level_by_class then
+									if c_data.last_level < lev then ok = false end
+								elseif self.level < lev then 
+									ok = false
+								end
+							end
+							if ok and req.talent then -- other prerequisite talents
+								for _, tid in ipairs(req.talent) do
+									if type(tid) == "table" then
+										if type(tid[2]) == "boolean" and tid[2] == false then
+											if self:knowTalent(tid[1]) then ok = false end
+										else
+											if tlev - 1 < tid[2] then ok = false end
+										end
+									else
+										if not self:knowTalent(tid) then ok = false end
+									end
+								end
+							end
+							if ok and req.special and not c_data.ignore_special then -- special checks
+								if not req.special.fct(self, t, 1) then ok = false	end
+							end
+							if ok and req.stat then -- check stat requirements, allocate stat points if needed
+								local need, needed, need_stats = 0, 0, {}
+								for s, v in pairs(req.stat) do
+									need = util.getval(v, tlev) - self:getStat(s)
+									if need > 0 then
+										need_stats[s] = need; needed = needed + need
+									end
+								end
+								if ok and needed <= self.unused_stats then -- allocate stats needed for the talent
+									for s, v in pairs(need_stats) do
+										local pts = self:incIncStat(s, v)
+										learn_stats[s] = (learn_stats[s] or 0) + pts
+										self.unused_stats = self.unused_stats - pts
+									end
+								else ok = false
+								end
+							end
+						end
+					end
+					ok = ok and self:learnTalent(t.id, true)
+					if ok then 
+--						print("\t *** learning "..(tt_def.generic and "GENERIC" or "CLASS").." talent", t.id, ok)
+						learn_tids[t.id] = (learn_tids[t.id] or 0) + 1
+						if tt_def.generic then self.unused_generics = self.unused_generics - 1 else self.unused_talents = self.unused_talents - 1 end
+						if t.mode == "sustained" and c_data.auto_sustain and not self:isTalentActive(t.id) then to_activate[t.id] = true end
+					else table.remove(t_choices, t_idx)
+					end
+				until ok
+				if not ok then -- no more talent choices possible for category
+					-- print("\t *** no talent choices in category:", tt)
+					table.remove(tt_choices, tt_idx)
+				end
+			else -- impossible to learn talents in the category
+				-- print("\t *** rejecting category:", tt)
+				table.remove(tt_choices, tt_idx)
+			end -- end category loop
+			fails = fails + 1
+		end
+	end
+	print("[Actor:levelupClass] ### levelup summary for", self.name, c_data.class, init_level, "-->", c_data.last_level)
+	print("[Actor:levelupClass] ### Talents Learned:")
+	table.print(learn_tids, "\t*\t")
+	print("[Actor:levelupClass] ### Talents forced Stats:", table.concatNice(table.to_strings(learn_stats, "%s: %s"), ", "))
+--	print((" *** auto_level complete: level: %s/%s remaining stats: %s talents: %s generics: %s categories: %s prodigies: %s"):format(c_data.last_level, new_level, self.unused_stats, self.unused_talents, self.unused_generics, self.unused_talents_types, self.unused_prodigies))
+
+	-- allocate any remaining stat points
+	if self.unused_stats > 0 and c_data.auto_stats then
+		print(" *** auto allocating", self.unused_stats, "remaining stat points")
+		self:learnStats(c_data.auto_stats, 5)
+	end
+	
+	-- restore unused stat and talent points
+	if not c_data.use_actor_points then
+		self.unused_stats, self.unused_talents, self.unused_generics = unpack(base_points)
+	end
+	-- turn on designated sustains
+	for tid, _ in pairs(to_activate) do self:forceUseTalent(tid, {ignore_energy=true}) end
+end
+
 function _M:resetToFull()
 	if self.dead then return end
 	self.life = self.max_life
@@ -3348,20 +3716,30 @@ function _M:resolveLevelTalents()
 		end
 		self.learn_tids = nil
 	end
-	if not self.start_level or not self._levelup_talents then return end
+--	if not self.start_level or not self._levelup_talents then return end
 
-	local maxfact = 1  -- Balancing parameter for levels > 50: maxtalent level = actorlevel/50*maxfact * normal max talent level
-	maxfact=math.max(maxfact,self.level/50)
+	if self.start_level and self._levelup_talents then
+		local maxfact = 1  -- Balancing parameter for levels > 50: maxtalent level = actorlevel/50*maxfact * normal max talent level
+		maxfact=math.max(maxfact,self.level/50)
 
-	for tid, info in pairs(self._levelup_talents) do
-		if not info.max or (self.talents[tid] or 0) < math.floor(info.max*maxfact) then
-			local last = info.last or self.start_level
-			if self.level - last >= info.every then
-				self:learnTalent(tid, true)
-				info.last = self.level
+		for tid, info in pairs(self._levelup_talents) do
+			if not info.max or (self.talents[tid] or 0) < math.floor(info.max*maxfact) then
+				local last = info.last or self.start_level
+				if self.level - last >= info.every then
+					self:learnTalent(tid, true)
+					info.last = self.level
+				end
 			end
 		end
 	end
+
+	-- automatically level up classes as defined
+	if self.auto_classes then
+		for i, c_data in ipairs(self.auto_classes) do
+			self:levelupClass(c_data)
+		end
+	end
+
 end
 
 function _M:levelup()
diff --git a/game/modules/tome/class/GameState.lua b/game/modules/tome/class/GameState.lua
index df21e3b4de..e9b05c2a30 100644
--- a/game/modules/tome/class/GameState.lua
+++ b/game/modules/tome/class/GameState.lua
@@ -1517,6 +1517,7 @@ function _M:entityFilterPost(zone, level, type, e, filter)
 				level = lev,
 				nb_rares = filter.random_elite.nb_rares or 1,
 				check_talents_level = true,
+				level_by_class = true,
 				user_post = filter.post,
 				post = function(b, data)
 					if data.level <= 20 then
@@ -1912,85 +1913,56 @@ function _M:createRandomZone(zbase)
 	return zone, boss
 end
 
---- Add one or more character classes to an actor, updating stats, talents, and equipment
+
+--- Add one or more character classes to an actor, updating stats, talents, and equipment,
+--  Updates autoleveling data so that class skills are advanced with level
+--	@see Actor:levelupClass
 --	@param b = actor(boss) to update
 --	@param data = optional parameters:
---	@param data.update_body a table of inventories to add, set true to add a full suite of inventories
---	@param data.force_classes = specific subclasses to apply first, ignoring restrictions
---		{"Rogue", "Necromancer", Corruptor = true, Bulwark = true, ...}
---		applied in order of numerical index, then randomly
---	@param data.nb_classes = random classes to add (in addition to any forced classes) <2>
--- 	@param data.class_filter = function(cdata, b) that must return true for any class picked.
+--	@field data.update_body a table of inventories to add, set true to add a full suite of inventories
+-- 	@field data.start_level: actor level to being leveling in the class(es) <1>
+-- 	@field data.level_rate: level of character class as % of actor level <100>
+--	@field data.force_classes = specific subclasses to apply first, ignoring restrictions
+--		{"Rogue" <=={Rogue = 100}>, {Necromancer = 75}, Corruptor = true <==100>, Bulwark = 50, ...}
+--		applied in order of numerical index, then randomly, numbers are the specified level_rate for each class
+--	@field data.nb_classes = random classes to add (in addition to any forced classes) <2>
+--		fractional classes are applied first with reduced level_rate
+-- 	@field data.class_filter = function(cdata, b) that must return true for any class picked.
 --		(cdata, b = subclass definition in engine.Birther.birth_descriptor_def.subclass, boss (before classes are applied))
---	@param data.no_class_restrictions set true to skip class compatibility checks <nil>
---	@param data.autolevel = autolevel scheme to use for stats (set false to keep current) <"random_boss">
---	@param data.spend_points = spend any unspent stat points (after adding all classes)
---	@param data.add_trees = {["talent tree name 1"]=true/mastery bonus, ["talent tree name 2"]=true/mastery bonus, ..} additional talent trees to learn
---	@param data.check_talents_level set true to enforce talent level restrictions <nil>
---	@param data.auto_sustain set true to activate sustained talents at birth <nil>
---	@param data.forbid_equip set true to not apply class equipment resolvers or equip inventory <nil>
---	@param data.loot_quality = drop table to use for equipment <"boss">
---	@param data.drop_equipment set true to force dropping of equipment <nil>
+--	@field data.no_class_restrictions set true to skip class compatibility checks <nil>
+--	@field data.autolevel = autolevel scheme to use for stats (set false to keep current) <"random_boss">
+--	@field data.spend_points = spend any unspent stat points (after adding all classes)
+--	@field data.add_trees = {["talent tree name 1"]=true/mastery bonus, ["talent tree name 2"]=true/mastery bonus, ..} additional talent trees to learn
+--	@field data.check_talents_level set true to enforce talent level restrictions based on class level <nil>
+--	@field data.auto_sustain set true to activate sustained talents at birth <nil>
+--	@field data.forbid_equip set true to ignore class equipment resolvers (and filters) or equip inventory <nil>
+--	@field data.loot_quality = drop table to use for equipment <"boss">
+--	@field data.drop_equipment set true to force dropping of equipment <nil>
 --	@param instant set true to force instant learning of talents and generating golem <nil>
 function _M:applyRandomClass(b, data, instant)
-	if not data.level then data.level = b.level end
+	if not data.level then data.level = b.level end -- use the level specified if needed
+
+print("[applyRandomClass] instant:", instant, "input data:") table.print(data, "__data__\t") -- debugging
 
 	------------------------------------------------------------
 	-- Apply talents from classes
 	------------------------------------------------------------
 	-- Apply a class
 	local Birther = require "engine.Birther"
-	b.learn_tids = {}
-	local function apply_class(class)
+	local function apply_class(class, level_rate)
 		local mclasses = Birther.birth_descriptor_def.class
 		local mclass = nil
 		for name, data in pairs(mclasses) do
 			if data.descriptor_choices and data.descriptor_choices.subclass and data.descriptor_choices.subclass[class.name] then mclass = data break end
 		end
-		if not mclass then return end
-
-		print("[applyRandomClass]", b.uid, b.name, "Adding class", class.name, mclass.name)
-		-- add class to list and build inherent power sources
-		b.descriptor = b.descriptor or {}
-		b.descriptor.classes = b.descriptor.classes or {}
-		table.append(b.descriptor.classes, {class.name})
-		
-		-- build inherent power sources and forbidden power sources
-		-- b.forbid_power_source --> b.not_power_source used for classes
-		b.power_source = table.merge(b.power_source or {}, class.power_source or {})
-		b.not_power_source = table.merge(b.not_power_source or {}, class.not_power_source or {})
-		-- update power source parameters with the new class
-		b.not_power_source, b.power_source = self:updatePowers(self:attrPowers(b, b.not_power_source), b.power_source)
-		print("   power types: not_power_source =", table.concat(table.keys(b.not_power_source),","), "power_source =", table.concat(table.keys(b.power_source),","))
-
-		-- Update/initialize base stats, set stats auto_leveling
-		if class.stats or b.auto_stats then
-			b.stats, b.auto_stats = b.stats or {}, b.auto_stats or {}
-			for stat, v in pairs(class.stats or {}) do
-				local stat_id = b.stats_def[stat].id
-				b.stats[stat_id] = (b.stats[stat_id] or 10) + v
-				for i = 1, v do b.auto_stats[#b.auto_stats+1] = stat_id end
-			end
-		end
-		if data.autolevel ~= false then b.autolevel = data.autolevel or "random_boss" end
-		
-		-- Class talent categories
-		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
-		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
-		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
-		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
-
-		-- Non-class talent categories
-		if data.add_trees then
-			for tt, d in pairs(data.add_trees) do
-				if not b:knowTalentType(tt) then
-					if type(d) ~= "number" then d = rng.range(1, 3)*0.1 end
-					b:learnTalentType(tt, true)
-					b:setTalentTypeMastery(tt, (b:getTalentTypeMastery(tt) or 1) + d)
-				end
-			end
+		if not mclass then
+			print("[applyRandomClass] ### ABORTING ###", b.uid, b.name, "No main class type for", class.name)
+			return
 		end
-		-- Add starting equipment
+
+		print("[applyRandomClass]", b.uid, b.name, "Adding class", class.name, mclass.name, "level_rate", level_rate)
+
+		-- Add starting equipment and update filters as needed
 		local apply_resolvers = function(k, resolver)
 			if type(resolver) == "table" and resolver.__resolver then
 				if resolver.__resolver == "equip" then
@@ -2007,6 +1979,10 @@ function _M:applyRandomClass(b, data, instant)
 						end
 						b[#b+1] = resolver
 					end
+				elseif resolver.__resolver == "auto_equip_filters" then
+					if not data.forbid_equip then
+						b[#b+1] = resolver
+					end
 				elseif resolver._allow_random_boss then -- explicitly allowed resolver
 					b[#b+1] = resolver
 				end
@@ -2024,81 +2000,18 @@ function _M:applyRandomClass(b, data, instant)
 		for k, resolver in pairs(mclass.copy or {}) do apply_resolvers(k, resolver) end
 		for k, resolver in pairs(class.copy or {}) do apply_resolvers(k, resolver) end
 
-		-- Assign a talent resolver for class starting talents (this makes them autoleveling)
-		local tres = nil
-		for k, resolver in pairs(b) do if type(resolver) == "table" and resolver.__resolver and resolver.__resolver == "talents" then tres = resolver break end end
-		if not tres then tres = resolvers.talents{} b[#b+1] = tres end
-		for tid, v in pairs(class.talents or {}) do
-			local t = b:getTalentFromId(tid)
-			if not t.no_npc_use and (not t.random_boss_rarity or rng.chance(t.random_boss_rarity)) then
-				local max = (t.points == 1) and 1 or math.ceil(t.points * 1.2)
-				local step = max / 50
-				tres[1][tid] = v + math.ceil(step * data.level)
-			end
-		end
-
-		-- Select additional talents from the class
-		local known_types = {}
-		for tt, d in pairs(b.talents_types) do
-			known_types[tt] = b:numberKnownTalent(tt)
-		end
-
-		local list = {}
-		for _, t in pairs(b.talents_def) do
-			if b.talents_types[t.type[1]] then
-				if t.no_npc_use or t.not_on_random_boss then
-					known_types[t.type[1]] = known_types[t.type[1]] + 1 -- allows higher tier talents to be learnt
-				else
-					local ok = true
-					if data.check_talents_level and rawget(t, 'require') then
-						local req = t.require
-						if type(req) == "function" then req = req(b, t) end
-						if req and req.level and util.getval(req.level, 1) > math.ceil(data.level/2) then
-							print("Random boss forbade talent because of level", t.name, t.id, data.level)
-							ok = false
-						end
-					end
-					if t.type[1]:find("/other$") then
-						print("Random boss forbase talent because category /other", t.name, t.id, t.type[1])
-						ok = false
-					end
-					if ok then list[t.id] = true end
-				end
-			end
-		end
-
-		local nb = 4 + 0.38*data.level^.75 -- = 11 at level 50
-		nb = math.max(rng.range(math.floor(nb * 0.7), math.ceil(nb * 1.3)), 1)
-		print("Adding "..nb.." random class talents to boss")
-
-		local count, fails = 0, 0
-		while count < nb do
-			local tid = rng.tableIndex(list, b.learn_tids)
-			if not tid or fails > nb * 5 then break end
-			local t = b:getTalentFromId(tid)
-			if t then
-				if t.type[2] and known_types[t.type[1]] < t.type[2] - 1 then -- not enough of talents of type
-					fails = fails + 1
-				else -- ok to add
-					count = count + 1
-					local max = (t.points == 1) and 1 or math.ceil(t.points * 1.2)
-					local step = max / 50
-					local lev = math.ceil(step * data.level)
-					print(count, " * talent:", tid, lev)
-					if instant then -- affected by game difficulty settings
-						if b:getTalentLevelRaw(tid) < lev then b:learnTalent(tid, true, lev - b:getTalentLevelRaw(tid)) end
-						if t.mode == "sustained" and data.auto_sustain then b:forceUseTalent(tid, {ignore_energy=true}) end
-					else  -- applied when added to the level (unaffected by game difficulty settings)
-						b.learn_tids[tid] = lev
-					end
-					known_types[t.type[1]] = known_types[t.type[1]] + 1
-					list[tid] = nil
-				end
-			else list[tid] = nil
-			end
-		end
-		print(" ** Finished adding", count, "of", nb, "random class talents")
-
+		b.auto_classes = b.auto_classes or {}
+		local c_data = {
+			class = class.name,
+			ttypes = data.add_trees, -- adds specified talent trees
+			spend_points = data.spend_points,
+			start_level = data.start_level,
+			level_rate = level_rate or 100,
+			auto_sustain = data.auto_sustain,
+			check_talents_level = data.check_talents_level,
+			level_by_class = data.level_by_class,
+		}
+		table.insert(b.auto_classes, c_data)
 		return true
 	end
 
@@ -2110,17 +2023,28 @@ function _M:applyRandomClass(b, data, instant)
 
 	-- Select classes
 	local classes = Birther.birth_descriptor_def.subclass
-	if data.force_classes then -- apply forced classes first, by index, then in random order
-		local c_list = table.clone(data.force_classes)
+	 -- apply forced classes first, by index, then in random order, extracting specified or implied level rates
+	if data.force_classes then
+		local c_list = table.clone(data.force_classes, true)
 		local force_classes = {}
 		for i, c_name in ipairs(c_list) do
-			force_classes[i] = c_list[i]
+			if type(c_name) == "table" then
+				force_classes[i] = c_list[i]
+			else
+				force_classes[i] = {[c_name]=data.level_rate or 100} -- default 100% level_rate
+			end
 			c_list[i] = nil
 		end
-		table.append(force_classes, table.shuffle(table.keys(c_list)))
-		for i, c_name in ipairs(force_classes) do
+		local rng_fc = {}
+		for c_name, lr in pairs(c_list) do
+			table.insert(rng_fc, {[c_name]=(type(lr) == "number" and lr or data.level_rate or 100)})
+		end
+		
+		table.append(force_classes, table.shuffle(rng_fc))
+		for i, cl in ipairs(force_classes) do
+			local c_name, lr = next(cl)
 			if classes[c_name] then
-				apply_class(table.clone(classes[c_name], true))
+				apply_class(table.clone(classes[c_name], true), lr)
 			else
 				print("  ###Forced class", c_name, "NOT DEFINED###")
 			end
@@ -2132,16 +2056,25 @@ function _M:applyRandomClass(b, data, instant)
 		end
 	end
 	
-	local to_apply = data.nb_classes or 2
+	-- apply random classes
+	local to_apply = data.nb_classes or 1.5 -- 1.5 is one primary class and one secondary class @ 50% stats/talents
+	print("[applyRandomClass] applying", to_apply, "classes at", data.level_rate, "%%")
 	while to_apply > 0 do
 		local c = rng.tableRemove(list)
 		if not c then break end --repeat attempts until list is exhausted
 		if data.no_class_restrictions or self:checkPowers(b, c) then  -- recheck power restricts here to account for any previously picked classes
-			if apply_class(table.clone(c, true)) then to_apply = to_apply - 1 end
+			-- if nb_classes is not an integer, apply partial classes first so that resolvers for later classes take precedence
+			local lr = to_apply - math.floor(to_apply)
+			lr = lr == 0 and 1 or lr
+			if apply_class(table.clone(c, true), math.max(lr*(data.level_rate or 100), 10)) then
+				to_apply = to_apply - lr
+				if instant then b:levelup() end -- force immediate level up of class to appropriate level (learning talents, etc.)
+			end
 		else
 			print("  * class", c.name, " rejected due to power source")
 		end
 	end
+
 	if data.spend_points then -- spend any remaining unspent stat points
 		repeat 
 			local last_stats = b.unused_stats
@@ -2150,28 +2083,30 @@ function _M:applyRandomClass(b, data, instant)
 	end
 end
 
+
 --- Creates a random Boss (or elite) actor
 --	@param base = base actor to add classes/talents to
 --	calls _M:applyRandomClass(b, data, instant) to add classes, talents, and equipment based on class descriptors
 --		handles data.nb_classes, data.force_classes, data.class_filter, ...
 --	optional parameters:
---	@param data.init = function(data, b) to run before generation
---	@param data.level = minimum level range for actor generation <1>
---	@param data.rank = rank <3.5-4>
---	@param data.life_rating = function(b.life_rating) <1.7 * base.life_rating + 4-9>
---	@param data.resources_boost = multiplier for maximum resource pool sizes <3>
---	@param data.talent_cds_factor = multiplier for all talent cooldowns <1>
---	@param data.ai = ai_type <"tactical" if rank>3 or base.ai>
---	@param data.ai_tactic = tactical weights table for the tactical ai <nil - generated based on talents>
---	@param data.no_loot_randart set true to not drop a randart <nil>
---	@param data.on_die set true to run base.rng_boss_on_die and base.rng_boss_on_die_custom on death <nil>
---	@param data.name_scheme <randart_name_rules.default>
---	@param data.post = function(b, data) to run last to finish generation
+--	@field data.init = function(data, b) to run before generation
+--	@field data.level = minimum level range for actor generation <1>
+--	@field data.nb_classes = number of random classes to add <1.75>
+--	@field data.rank = rank <3.5-4>
+--	@field data.life_rating = function(b.life_rating) <1.7 * base.life_rating + 4-9>
+--	@field data.resources_boost = multiplier for maximum resource pool sizes <3>
+--	@field data.talent_cds_factor = multiplier for all talent cooldowns <1>
+--	@field data.ai = ai_type <"tactical" if rank>3 or base.ai>
+--	@field data.ai_tactic = tactical weights table for the tactical ai <nil - generated based on talents>
+--	@field data.no_loot_randart set true to not drop a randart <nil>
+--	@field data.on_die set true to run base.rng_boss_on_die and base.rng_boss_on_die_custom on death <nil>
+--	@field data.name_scheme <randart_name_rules.default>
+--	@field data.post = function(b, data) to run last to finish generation
 function _M:createRandomBoss(base, data)
 	local b = base:clone()
 	data = data or {level=1}
 	if data.init then data.init(data, b) end
-	data.nb_classes = data.nb_classes or 2
+	data.nb_classes = data.nb_classes or 1.75 -- Default one primary class @100% and one secondary class @ 75% level
 
 	------------------------------------------------------------
 	-- Basic stuff, name, rank, ...
@@ -2230,6 +2165,7 @@ function _M:createRandomBoss(base, data)
 	-- Leveling stats
 	b.autolevel = "random_boss"
 	b.auto_stats = {}
+	b.max_level = nil
 
 	-- Update default equipment, if any, to "boss" levels
 	for k, resolver in ipairs(b) do
@@ -2263,12 +2199,14 @@ function _M:createRandomBoss(base, data)
 	------------------------------------------------------------
 	-- Apply talents from classes
 	------------------------------------------------------------
-	self:applyRandomClass(b, data)
-
+	-- apply classes (instant to level up stats/talents before equipment is resolved)
+	self:applyRandomClass(b, data, true)
+	
 	b.rnd_boss_on_added_to_level = b.on_added_to_level
 	b._rndboss_resources_boost = data.resources_boost or 3
 	b._rndboss_talent_cds = data.talent_cds_factor
 	b.on_added_to_level = function(self, ...)
+		self:levelup() -- this triggers processing of auto_classes to learn class talents and gain appropriate stats
 		self:check("birth_create_alchemist_golem")
 		self:check("rnd_boss_on_added_to_level", ...)
 		self.rnd_boss_on_added_to_level = nil
diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua
index 8cb5f86e1a..01f814fbcf 100644
--- a/game/modules/tome/class/NPC.lua
+++ b/game/modules/tome/class/NPC.lua
@@ -481,26 +481,15 @@ function _M:addedToLevel(level, x, y)
 		if game.difficulty == game.DIFFICULTY_NIGHTMARE then
 			talent_mult = 1.3
 			life_mult = 1.5
+			nb_classes = util.bound(self.rank - 3.5, 0, 1) -- up to 1 extra class
 		elseif game.difficulty == game.DIFFICULTY_INSANE then
 			talent_mult = 1.8
 			life_mult = 2.0
-			if self.rank >= 3.5 then
-				if self.rank >= 10 then nb_classes = 3
-				elseif self.rank >= 5 then nb_classes = 2
-				else nb_classes = 1
-				end
-			end
+			nb_classes = util.bound(self.rank - 3, 0, 2) -- up to 2 extra classes
 		elseif game.difficulty == game.DIFFICULTY_MADNESS then
 			talent_mult = 2.7
 			life_mult = 3.0
-			if self.rank >= 3.5 then
-				if self.rank >= 10 then nb_classes = 5
-				elseif self.rank >= 5 then nb_classes = 3
-				elseif self.rank >= 4 then nb_classes = 2
-				else nb_classes = 1
-				end
-			end
-	
+			nb_classes = util.bound((self.rank - 3)*1.5, 0, 3) -- up to 3 extra classes
 		end
 		if talent_mult ~= 1 then
 			-- increase level of innate talents
@@ -510,13 +499,16 @@ function _M:addedToLevel(level, x, y)
 					self:learnTalent(tid, true, math.floor(lev*(talent_mult - 1)))
 				end
 			end
-			-- add extra character classes
-			if nb_classes > 0 and not self.randboss and not self.no_difficulty_random_class then
-				-- Note: talent levels from added classes are not adjusted for difficulty
+			-- add the extra character classes (halved for randbosses)
+			if nb_classes > 0 and not self.no_difficulty_random_class then
+				-- Note: talent levels from added classes are not adjusted for difficulty directly
 				-- This means that the NPC's innate talents are generally higher level, preserving its "character"
-				local data = {auto_sustain=true, forbid_equip=false, nb_classes=nb_classes, update_body=true, spend_points=true, autolevel=nb_classes<2 and self.autolevel or "random_boss"}
+				if self.randboss then nb_classes = nb_classes/2 end
+				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"}
 				game.state:applyRandomClass(self, data, true)
+				self[#self+1] = resolvers.talented_ai_tactic("instant") -- regenerate AI TACTICS with the new class(es)
 				self:resolve() self:resolve(nil, true)
+				self:resetToFull()
 			end
 			
 			-- increase maximum life
diff --git a/game/modules/tome/dialogs/debug/RandomActor.lua b/game/modules/tome/dialogs/debug/RandomActor.lua
index a9a07cf222..effd2d2dbd 100644
--- a/game/modules/tome/dialogs/debug/RandomActor.lua
+++ b/game/modules/tome/dialogs/debug/RandomActor.lua
@@ -52,19 +52,23 @@ lines, fname, lnum = DebugConsole:functionHelp(game.state.createRandomBoss)
 _M.data_help = "#GOLD#DATA HELP#LAST# "..formatHelp(lines, fname, lnum)
 lines, fname, lnum = DebugConsole:functionHelp(game.state.applyRandomClass)
 _M.data_help = _M.data_help.."\n#GOLD#DATA HELP#LAST# "..formatHelp(lines, fname, lnum)
+lines, fname, lnum = DebugConsole:functionHelp(mod.class.Actor.levelupClass)
+_M.data_help = _M.data_help.."\n#GOLD#DATA HELP#LAST# "..formatHelp(lines, fname, lnum)
 
 function _M:init()
 	engine.ui.Dialog.init(self, "DEBUG -- Create Random Actor", 1, 1)
 
 	local tops={0}	self.tops = tops
 	
-	if not _M._base_actor then self:generateBase() end
+--	if not _M._base_actor then self:generateBase() end
 	
 	local dialog_txt = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font,
 	text=([[Randomly generate actors subject to a filter and/or create random bosses according to a data table.
-Filters and data are interpreted by either game.zone:checkFilter or game.state:createRandomBoss and game.state:applyRandomClass respectively,
-within the _G environment (used by the Lua Console) using the current zone's #LIGHT_GREEN#npc_list#LAST#.  Press #GOLD#'F1'#LAST# for help.
-Mouse over controls for actor preview.  (Actors may be adjusted when placed on to the level.)
+Filters are interpreted by game.zone:checkFilter.
+#ORANGE#Boss Data:#LAST# is interpreted by game.state:createRandomBoss, game.state:applyRandomClass, and Actor.levelupClass.
+Generation is performed within the _G environment (used by the Lua Console) using the current zone's #LIGHT_GREEN#npc_list#LAST#.
+Press #GOLD#'F1'#LAST# for help.
+Mouse over controls for an actor preview (which may be further adjusted when placed on to the level).
 (Press #GOLD#'L'#LAST# to lua inspect or #GOLD#'C'#LAST# to open the character sheet.)
 
 The #LIGHT_BLUE#Base Filter#LAST# is used to filter the actor randomly generated.]]):format(), can_focus=false}
@@ -345,6 +349,7 @@ function _M:generateBase()
 end
 
 -- generate random boss
+-- note: difficulty effects will not be reapplied when a base actor is used
 function _M:generateBoss()
 	local base = _M._base_actor
 	if not base then
diff --git a/game/modules/tome/dialogs/debug/SummonCreature.lua b/game/modules/tome/dialogs/debug/SummonCreature.lua
index 3a58fa4e30..e2681afc6e 100644
--- a/game/modules/tome/dialogs/debug/SummonCreature.lua
+++ b/game/modules/tome/dialogs/debug/SummonCreature.lua
@@ -130,6 +130,8 @@ function _M:placeCreature(m)
 					end
 					local Dstring = m.getDisplayString and m:getDisplayString() or ""
 					game.log("#LIGHT_BLUE#Added %s[%s]%s at (%d, %d)", Dstring, m.uid, m.name, x, y)
+					-- special cases
+					if m.alchemy_golem then game.zone:addEntity(game.level, m.alchemy_golem, "actor", util.findFreeGrid(m.x, m.y, 3, false, {[engine.Map.ACTOR]=true})) end
 				end
 			end
 			game:registerDialog(self)
diff --git a/game/modules/tome/resolvers.lua b/game/modules/tome/resolvers.lua
index e761aa1838..284da639ab 100644
--- a/game/modules/tome/resolvers.lua
+++ b/game/modules/tome/resolvers.lua
@@ -199,7 +199,7 @@ function resolvers.resolveObject(e, filter, do_wear, tries)
 					end
 				end
 			end
-			if not worn then print("General Object resolver]", o.uid, o.name, "COULD NOT BE WORN") end
+			if not worn then print("[General Object resolver]", o.uid, o.name, "COULD NOT BE WORN") end
 		end
 		-- if not worn, add to main inventory unless do_wear == false
 		if do_wear ~= false then
@@ -808,7 +808,7 @@ function resolvers.calc.sustains_at_birth(_, e)
 	e.on_added = function(self)
 		for tid, _ in pairs(self.talents) do
 			local t = self:getTalentFromId(tid)
-			if t and t.mode == "sustained" then
+			if t and t.mode == "sustained" and not self:isTalentActive(tid) then
 				self.energy.value = game.energy_to_act
 				self:useTalent(tid, nil, nil, nil, nil, true)
 			end
@@ -913,10 +913,11 @@ end
 --- Resolve tactical ai weights based on talents known
 --	mostly to make sure randbosses have sensible ai_tactic tables
 --	this tends to make npc's slightly more aggressive/defensive depending on their talents
---	@param method = function to be applied to generating the ai_tactic table <not implemented>
--- 	@param tactic_emphasis = average weight of favored tactics <1.5>
---	@param weight_power = smoothing factor to balance out weights <0.5>
---	applied with "on_added_to_level"
+--	@param method = function to be applied to generate the ai_tactic table <generally not implemented>
+--		tactics are updated with "on_added_to_level"
+--		use "instant" to resolve the tactics immediately using the "simple_recursive" method
+-- 	@param tactic_emphasis = average weight of favored tactics, higher values make the NPC more aggressive or defensive <1.5>
+--	@param weight_power = smoothing factor (> 0) to balance out weights <0.5>
 function resolvers.talented_ai_tactic(method, tactic_emphasis, weight_power)
 	local method = method or "simple_recursive"
 	return {__resolver="talented_ai_tactic", method, tactic_emphasis or 1.5, weight_power, __resolve_last=true,
@@ -931,9 +932,10 @@ function resolvers.calc.talented_ai_tactic(t, e)
 	end
 	--print("talented_ai_tactic resolver setting up on_added_to_level function")
 	--print(debug.traceback())
-	e.on_added_to_level = function(e, level, x, y)
+	local on_added = function(e, level, x, y)
 		print("running talented_ai_tactic resolver on_added_to_level function for", e.uid, e.name)
 		local t = e.__ai_tactic_resolver
+		if not t then print("talented_ai_tactic: No resolver table. Aborting") return end
 		e.__ai_tactic_resolver = nil
 		if t.old_on_added_to_level then t.old_on_added_to_level(e, level, x, y) end
 		
@@ -942,7 +944,7 @@ function resolvers.calc.talented_ai_tactic(t, e)
 			return t[1](t, e, level)
 		end
 		-- print("  # talented_ai_tactic resolver function for", e.name, "level=", e.level, e.uid)
-		local tactic_emphasis = t[2] or t.tactic_emphasis or 2 --want average tactic weight to be 2
+		local tactic_emphasis = t[2] or t.tactic_emphasis or 1.5 --desired average tactic weight
 		local weight_power = t[3] or t.weight_power or 0.5 --smooth out tactical weights
 		local tacs_offense = {attack=1, attackarea=1, areaattack=1}
 		local tacs_close = {closein=1, go_melee=1}
@@ -1078,13 +1080,19 @@ function resolvers.calc.talented_ai_tactic(t, e)
 		tactic.tactical_sum=tactical
 		tactic.count = count
 		tactic.level = e.level
-		tactic.type = "computed"
+		tactic.type = "simple_recursive"
 		--- print("### talented_ai_tactic resolver ai_tactic table:")
 		--- for tac, wt in pairs(tactic) do print("    ##", tac, wt) end
 		e.ai_tactic = tactic
 --		e.__ai_tactic_resolver = nil
 		return tactic
 	end
+	if t[1] == "instant" then
+		e.__ai_tactic_resolver = t
+		on_added(e, level or game.level, e.x, e.y)
+	else
+		e.on_added_to_level = on_added
+	end
 end
 
 --- Racial Talents resolver
-- 
GitLab