diff --git a/game/engines/default/engine/Actor.lua b/game/engines/default/engine/Actor.lua index 2856841af79be9059bba4e28359a4ab638de0e11..dad740f1628574be331738d11b0bdd0008692a09 100644 --- a/game/engines/default/engine/Actor.lua +++ b/game/engines/default/engine/Actor.lua @@ -69,6 +69,32 @@ end function _M:setTarget(target) end +--- cloneActor default alt_node fields (controls fields copied by cloneCustom) +-- modules should update this as needed +_M.clone_nodes = {player=false, x=false, y=false, + fov_computed=false, fov={v={actors={}, actors_dist={}}}, distance_map={v={}}, + _mo=false, _last_mo=false, add_mos=false, add_displays=false, + shader=false, shader_args=false, +} +--- cloneActor default fields (merged by _M.cloneActor with cloneCustom) +-- modules may define this as a table to automatically merge into cloned actors +_M.clone_copy = nil + +--- Special version of cloneFull that clones an Actor, automatically managing duplication of some fields +-- uses class.CloneCustom +-- @param[optional, type=?table] post_copy a table merged into the cloned actor +-- updated with self.clone_copy if it is defined +-- @param[default=self.clone_nodes, type=?table] alt_nodes a table containing parameters for cloneCustom +-- to be merged with self.clone_nodes +-- @return the cloned actor +function _M:cloneActor(post_copy, alt_nodes) + alt_nodes = table.merge(alt_nodes or {}, self.clone_nodes, true) + if post_copy or self.clone_copy then post_copy = post_copy or {} table.update(post_copy, self.clone_copy or {}, true) end + local a = self:cloneCustom(alt_nodes, post_copy) + a:removeAllMOs() + return a, post_copy +end + --- Setup minimap color for this entity -- You may overload this method to customize your minimap -- @param mo diff --git a/game/engines/default/engine/Birther.lua b/game/engines/default/engine/Birther.lua index f18617b9d447c769b2ec16eedc6fc8fe4652133c..928f3989e5f6652bb56f062a3a9ed5259ccb5963 100644 --- a/game/engines/default/engine/Birther.lua +++ b/game/engines/default/engine/Birther.lua @@ -360,14 +360,14 @@ function _M:apply() self.actor.descriptor = {} local stats, inc_stats = {}, {} for i, d in ipairs(self.descriptors) do - print("[BIRTHER] Applying descriptor "..(d.name or "none")) + print("[BIRTHER] Applying descriptor "..(d.type or "untyped").."."..(d.name or "none")) self.actor.descriptor[d.type or "none"] = (d.name or "none") if d.copy then local copy = table.clone(d.copy, true) -- Append array part while #copy > 0 do - local f = table.remove(copy) + local f = table.remove(copy, 1) table.insert(self.actor, f) end -- Copy normal data @@ -377,7 +377,7 @@ function _M:apply() local copy = table.clone(d.copy_add, true) -- Append array part while #copy > 0 do - local f = table.remove(copy) + local f = table.remove(copy, 1) table.insert(self.actor, f) end -- Copy normal data @@ -410,6 +410,7 @@ function _M:apply() end if d.talents then for tid, lev in pairs(d.talents) do + print("[BIRTHER} learning talent", tid, lev) for i = 1, lev do self.actor:learnTalent(tid, true) end diff --git a/game/engines/default/engine/DebugConsole.lua b/game/engines/default/engine/DebugConsole.lua index cc53607c51455c0c8250cf7c6f8c49541432306e..6ad81783fd3e053c594c4b925af562d86af1c99a 100644 --- a/game/engines/default/engine/DebugConsole.lua +++ b/game/engines/default/engine/DebugConsole.lua @@ -43,6 +43,8 @@ history = { [[< Ctrl+A or Home :=: Move the cursor to the beginning of the line >]], [[< Ctrl+E or End :=: Move the cursor to the end of the line >]], [[< Ctrl+K or Ctrl+End :=: Move the cursor to the end of the line >]], + [[< Ctrl+Backspace :=: Delete to beginning of line >]], + [[< Ctrl+Del :=: Delete to end of line >]], [[< Up/down arrows :=: Move between previous/later executed lines >]], [[< Ctrl+Space :=: Print help for the function to the left of the cursor >]], [[< Ctrl+Shift+Space :=: Print the entire definition for the function >]], @@ -216,13 +218,19 @@ function _M:init() end, _BACKSPACE = function() if _M.line_pos > 0 then - _M.line = _M.line:sub(1, _M.line_pos - 1) .. _M.line:sub(_M.line_pos + 1) - _M.line_pos = _M.line_pos - 1 + local st = core.key.modState("ctrl") and 0 or _M.line_pos - 1 + for i = _M.line_pos - 1, st, -1 do + _M.line = _M.line:sub(1, _M.line_pos - 1) .. _M.line:sub(_M.line_pos + 1) + _M.line_pos = _M.line_pos - 1 + end end self.changed = true end, _DELETE = function() - _M.line = _M.line:sub(1, _M.line_pos) .. _M.line:sub(_M.line_pos + 2) + local st = core.key.modState("ctrl") and #_M.line or _M.line_pos + for i = _M.line_pos, st do + _M.line = _M.line:sub(1, _M.line_pos) .. _M.line:sub(_M.line_pos + 2) + end self.changed = true end, [{"_END", "ctrl"}] = function() @@ -452,7 +460,7 @@ end -- @param[type=boolean] verbose give extra junk function _M:functionHelp(func, verbose) if type(func) ~= "function" then return nil, "Can only give help on functions." end - local info = debug.getinfo(func) + local info = debug.getinfo(func, "S") -- Check the path exists local fpath = string.gsub(info.source,"@","") if not fs.exists(fpath) then return nil, ([[%s does not exist.]]):format(fpath) end diff --git a/game/engines/default/engine/Entity.lua b/game/engines/default/engine/Entity.lua index 0f1739143f5613b6c3095e708dc7c09daf4f580a..cebd77f58acf8ed2ca80d900df2eac7efdbd4bac 100644 --- a/game/engines/default/engine/Entity.lua +++ b/game/engines/default/engine/Entity.lua @@ -712,14 +712,28 @@ function _M:toScreen(tiles, x, y, w, h, a, allow_cb, allow_shader) core.map.mapObjectsToScreen(x, y, w, h, a, allow_cb, allow_shader, unpack(list)) end ---- Resolves an entity --- This is called when generating the final clones of an entity for use in a level. --- This can be used to make random enchants on objects, random properties on actors, ... --- by default this only looks for properties with a table value containing a __resolver field --- @param[type=?table] t table defaults to self --- @param[type=?boolean] last resolve last --- @param[type=?boolean] on_entity --- @param[type=?table] key_chain stores keys, defaults to {} +--- Resolves an entity, finishing resolvers previously defined for it +-- Called when generating the final clones of (instantiating) a prototype entity for use in the game +-- This may be used to make random enchants on objects, add random properties on actors, etc. +-- Recursively searches for all property fields assigned a resolver table, assigning final values to those properties +-- (resolver tables are tables containing a __resolver field by default) +-- @see resolvers.lua +-- @param[type=?table] t table to search recursively for resolver tables (defaults to self) +-- @param[type=?boolean] last handle resolvers with .__resolve_last set (which are skipped if this is not set) +-- @param[type=?table] on_entity alternate entity for which to evaluate resolvers +-- @param[type=?table] key_chain (defaults to {}) contains keys recursively traversed while resolving +-- During the recursive search: +-- subtables with .__ATOMIC or .__CLASSNAME are skipped +-- the key_chain table is appended with each recursive key +-- resolver tables (containing .__resolver) are evaluated +-- resolvers are 2-part functions: +-- resolver.foo(...) (called when creating the entity prototype) assigns a resolver table to a property field within the entity +-- The resolver table contains .__resolver == "foo" and all other data needed to create the final value for the property field when the final entity instance is prepared (performed here) +--- If the resolver table contains .__resolve_instant, it will be resolved immediately (before the next level of recursion) +-- resolver.calc.foo(t, e) (called here) calculates the final value for ("resolves") the property field where: +-- t = table generated by resolver.foo and stored in the property field +-- e = the entity on which to perform the changes +-- the property field is set to the return value of this function function _M:resolve(t, last, on_entity, key_chain) t = t or self key_chain = key_chain or {} @@ -737,8 +751,8 @@ function _M:resolve(t, last, on_entity, key_chain) -- Then we handle it, this is because resolvers can modify the list with their returns, or handlers, so we must make sure to not modify the list we are iterating over local r for k, e in pairs(list) do - if type(e) == "table" and e.__resolver then - end +-- if type(e) == "table" and e.__resolver then +-- end if type(e) == "table" and e.__resolver and (not e.__resolve_last or last) then if not resolvers.calc[e.__resolver] then error("missing resolver "..e.__resolver.." on entity "..tostring(t).." key "..table.concat(key_chain, ".")) end r = resolvers.calc[e.__resolver](e, on_entity or self, self, t, k, key_chain) diff --git a/game/engines/default/engine/Module.lua b/game/engines/default/engine/Module.lua index 808d25e845b1930ab71dfd1cdb929d363f436b54..0ce99855a438f2a0a6177fbd9966bbc318bebc4b 100644 --- a/game/engines/default/engine/Module.lua +++ b/game/engines/default/engine/Module.lua @@ -1016,6 +1016,7 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info) profile:addStatFields(unpack(mod.profile_stats_fields or {})) profile:setConfigsBatch(true) profile:loadModuleProfile(mod.short_name, mod) + profile:incrLoadProfile(mod) profile:currentCharacter(mod.full_version_string, "game did not tell us") UIBase:clearCache() @@ -1068,28 +1069,30 @@ function _M:instanciate(mod, name, new_game, no_reboot, extra_module_info) -- Add user chat if needed if mod.allow_userchat and _G.game.key then profile.chat:setupOnGame() - if not config.settings.chat or not config.settings.chat.channels or not config.settings.chat.channels[mod.short_name] then - if type(mod.allow_userchat) == "table" then - for _, chan in ipairs(mod.allow_userchat) do - profile.chat:join(chan) + profile:onAuth(function() + if not config.settings.chat or not config.settings.chat.channels or not config.settings.chat.channels[mod.short_name] then + if type(mod.allow_userchat) == "table" then + for _, chan in ipairs(mod.allow_userchat) do + profile.chat:join(chan) + end + if mod.allow_userchat[1] then profile.chat:selectChannel(mod.allow_userchat[1]) end + else + profile.chat:join(mod.short_name) + profile.chat:join(mod.short_name.."-spoiler") + profile.chat:join("global") + profile.chat:selectChannel(mod.short_name) end - if mod.allow_userchat[1] then profile.chat:selectChannel(mod.allow_userchat[1]) end + print("Joining default channels") else - profile.chat:join(mod.short_name) - profile.chat:join(mod.short_name.."-spoiler") - profile.chat:join("global") - profile.chat:selectChannel(mod.short_name) - end - print("Joining default channels") - else - local def = false - for c, _ in pairs(config.settings.chat.channels[mod.short_name]) do - profile.chat:join(c) - if c == mod.short_name then def = true end + local def = false + for c, _ in pairs(config.settings.chat.channels[mod.short_name]) do + profile.chat:join(c) + if c == mod.short_name then def = true end + end + if def then profile.chat:selectChannel(mod.short_name) else profile.chat:selectChannel( (next(config.settings.chat.channels[mod.short_name])) ) end + print("Joining selected channels") end - if def then profile.chat:selectChannel(mod.short_name) else profile.chat:selectChannel( (next(config.settings.chat.channels[mod.short_name])) ) end - print("Joining selected channels") - end + end) end -- Disable the profile if ungood diff --git a/game/engines/default/engine/PlayerProfile.lua b/game/engines/default/engine/PlayerProfile.lua index 3a5ca60d17d01608b715b1db9719348489c7b142..782974397962e3b64b753cd3d1c77eb6f4e0c035 100644 --- a/game/engines/default/engine/PlayerProfile.lua +++ b/game/engines/default/engine/PlayerProfile.lua @@ -79,6 +79,7 @@ function _M:init() self.generic = {} self.modules = {} self.evt_cbs = {} + self.data_log = {log={}} self.stats_fields = {} local checkstats = function(self, field) return self.stats_fields[field] end self.config_settings = @@ -236,6 +237,7 @@ function _M:loadModuleProfile(short_name, mod_def) -- Delay when we are currently saving if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("loadModuleProfile", function() self:loadModuleProfile(short_name) end) return end + local def = mod_def.profile_defs or {} local function load(online) local pop = self:mountProfile(online, short_name) local d = "/current-profile/modules/"..short_name.."/" @@ -251,6 +253,12 @@ function _M:loadModuleProfile(short_name, mod_def) else self.modules[short_name][field] = self.modules[short_name][field] or {} self:loadData(f, self.modules[short_name][field]) + if def[field].incr_only then + if not self.modules[short_name][field].incr_only then + print("[PROFILE] Old non incremental data for "..field..": discarding") + self.modules[short_name][field] = {} + end + end end end end @@ -319,6 +327,7 @@ function _M:saveModuleProfile(name, data, nosync, nowrite) if module == "boot" then return end core.game.resetLocale() if not game or not game.__mod_info.profile_defs then return end + if game.__mod_info.profile_defs[name].incr_only then print("[PROFILE] data in incr only mode but called with saveModuleProfile", name) return end -- Delay when we are currently saving if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("saveModuleProfile", function() self:saveModuleProfile(name, data, nosync) end) return end @@ -364,6 +373,49 @@ function _M:saveModuleProfile(name, data, nosync, nowrite) if not nosync and not game.__mod_info.profile_defs[name].no_sync then self:setConfigs(module, name, data) end end +--- Loads the incremental log data +function _M:incrLoadProfile(mod_def) + if not mod_def or not mod_def.short_name then return end + local pop = self:mountProfile(true) + local file = "/current-profile/modules/"..mod_def.short_name.."/incr.log" + if fs.exists(file) then + local f, err = loadfile(file) + if not f and err then + print("Error loading incr log", file, err) + else + self:loadData(f, self.data_log) + end + end + self:umountProfile(true, pop) +end + +--- Saves a incr profile data +function _M:incrDataProfile(name, data) + if module == "boot" then return end + core.game.resetLocale() + if not game or not game.__mod_info.profile_defs then return end + if not game.__mod_info.profile_defs[name].incr_only then print("[PROFILE] data in non-incr only mode but called with incrDataProfile", name) return end + + -- Delay when we are currently saving + if savefile_pipe and savefile_pipe.saving then savefile_pipe:pushGeneric("incrDataProfile", function() self:incrDataProfile(name, data) end) return end + + local module = self.mod_name + + -- Check for readability + local dataenv = self.data_log.log + dataenv[#dataenv+1] = {module=game.__mod_info.short_name, kind=name, data=data} + + local pop = self:mountProfile(true, module) + local f = fs.open("/modules/"..module.."/incr.log", "w") + if f then + f:write(serialize(self.data_log)) + f:close() + end + self:umountProfile(true, pop) + + self:syncIncrData() +end + function _M:checkFirstRun() local result = self.generic.firstrun if not result then @@ -483,6 +535,12 @@ function _M:waitFirstAuth(timeout) end end +function _M:onAuth(fct) + if self.auth then fct() return end + self.on_auth_cb = self.on_auth_cb or {} + self.on_auth_cb[#self.on_auth_cb+1] = fct +end + function _M:eventAuth(e) self.waiting_auth = false self.connected = true @@ -491,6 +549,8 @@ function _M:eventAuth(e) self.auth = e.ok:unserialize() print("[PROFILE] Main thread got authed", self.auth.name) self:getConfigs("generic", function(e) self:syncOnline(e.module) end) + for _, fct in ipairs(self.on_auth_cb or {}) do fct() end + self.on_auth_cb = nil else self.auth_last_error = e.reason or "unknown" end @@ -503,6 +563,16 @@ function _M:eventGetNews(e) end end +function _M:eventIncrLogConsume(e) + local module = game.__mod_info.short_name + if not module then return end + print("[PROFILE] Server accepted our incr log, deleting") + local pop = self:mountProfile(true, module) + fs.delete("/modules/"..module.."/incr.log") + self:umountProfile(true, pop) + self.data_log.log = {} +end + function _M:eventGetConfigs(e) local data = zlib.decompress(e.data):unserialize() local module = e.module @@ -542,11 +612,13 @@ end function _M:eventConnected(e) if game and type(game) == "table" and game.log then game.log("#YELLOW#Connection to online server established.") end + print("[PlayerProfile] eventConnected") self.connected = true end function _M:eventDisconnected(e) if game and type(game) == "table" and game.log and self.connected then game.log("#YELLOW#Connection to online server lost, trying to reconnect.") end + print("[PlayerProfile] eventDisconnected") self.connected = false end @@ -607,14 +679,14 @@ function _M:getConfigs(module, cb, mod_def) if not self.auth then return end self.evt_cbs.GetConfigs = cb if module == "generic" then - for k, _ in pairs(generic_profile_defs) do - if not _.no_sync then + for k, def in pairs(generic_profile_defs) do + if not def.no_sync then core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k}) end end else - for k, _ in pairs((mod_def or game.__mod_info).profile_defs or {}) do - if not _.no_sync then + for k, def in pairs((mod_def or game.__mod_info).profile_defs or {}) do + if not def.no_sync and not def.incr_only then core.profile.pushOrder(table.serialize{o="GetConfigs", module=module, kind=k}) end end @@ -625,6 +697,15 @@ function _M:setConfigsBatch(v) core.profile.pushOrder(table.serialize{o="SetConfigsBatch", v=v and true or false}) end +function _M:syncIncrData() + self:waitFirstAuth() + if not self.auth then return end + local module = game and game.__mod_info.short_name + if not module then return end + + core.profile.pushOrder(table.serialize{o="SendIncrLog", data=zlib.compress(table.serialize(self.data_log.log))}) +end + function _M:setConfigs(module, name, data) self:waitFirstAuth() if not self.auth then return end @@ -660,7 +741,7 @@ function _M:syncOnline(module, mod_def) end else for k, def in pairs((mod_def or game.__mod_info).profile_defs or {}) do - if not def.no_sync and def.export and sync[k] then + if not def.no_sync and not def.incr_only and def.export and sync[k] then local f = def.export local ret = {} setfenv(f, setmetatable({add=function(d) ret[#ret+1] = d end}, {__index=_G})) diff --git a/game/engines/default/engine/Savefile.lua b/game/engines/default/engine/Savefile.lua index c6907fccc5f7c7a824f244929a87d1ca365c5cd8..b30a5893e9b05d9bc2e63bd7bbba0b8052bbf8fe 100644 --- a/game/engines/default/engine/Savefile.lua +++ b/game/engines/default/engine/Savefile.lua @@ -698,17 +698,22 @@ function _M:loadEntity(name) local loadedEntity = self:loadReal("main") -- Delay loaded must run - for i, o in ipairs(self.delayLoad) do --- print("loader executed for class", o, o.__CLASSNAME) - o:loaded() - end + local ok = false + pcall(function() + for i, o in ipairs(self.delayLoad) do +-- print("loader executed for class", o, o.__CLASSNAME) + o:loaded() + end + ok = true + end) -- We check for the server return, while we loaded the save it probably responded so we do not wait at all - if not checker() then self:badMD5Load() end + if ok and not checker() then self:badMD5Load() end popup:done() fs.umount(path) + if not ok then return false end return loadedEntity end diff --git a/game/engines/default/engine/Zone.lua b/game/engines/default/engine/Zone.lua index 61df2a74af0129657a44a995d2e41bc8ec92729f..c441424c95f458e187c85cfa08046af659f67f0e 100644 --- a/game/engines/default/engine/Zone.lua +++ b/game/engines/default/engine/Zone.lua @@ -263,11 +263,30 @@ function _M:computeRarities(type, list, level, filter, add_level, rarity_field) end --- Checks an entity against a filter -function _M:checkFilter(e, filter, type) - if e.unique and game.uniques[e.__CLASSNAME.."/"..e.unique] then print("refused unique", e.name, e.__CLASSNAME.."/"..e.unique) return false end +-- @param e: the entity to check +-- @param filter: the filter to use, a table specifying the checks to perform on the entity +-- @param typ: the type of entity, one of "actor", "projectie", "object", "trap", "terrain", "grid", "trigger" +-- @return true if the entity passes all of the filter checks +-- filter fields interpreted: +-- allow_uniques: don't reject existing uniques +-- unique: e[unique] must be defined +-- ignore: a filter specifying what entities to be specifically rejected +-- type: e[type] must match exactly +-- subtype: e[subtype] must match exactly +-- name: e[name] must match exactly +-- define_as: e.define_as must match exactly +-- properties: table of keys, e[key] MUST BE defined for all +-- not_properties: table of keys, e[key] MUST NOT BE defined for all +-- special: function(e, filter) that must return true +-- max_ood: perform an Out of Depth check (requires resolvers.current_level + max_ood > e.level_range[1]) +-- Rejects existing uniques +-- If it is defined, e.checkFilter(filter) must return true +-- If it is defined, self:check_filter(e, filter, typ) must return true +function _M:checkFilter(e, filter, typ) + if e.unique and not (filter and filter.allow_uniques) and game.uniques[e.__CLASSNAME.."/"..e.unique] then print("refused unique", e.name, e.__CLASSNAME.."/"..e.unique) return false end if not filter then return true end - if filter.ignore and self:checkFilter(e, filter.ignore, type) then return false end + if filter.ignore and self:checkFilter(e, filter.ignore, typ) then return false end print("Checking filter", filter.type, filter.subtype, "::", e.type,e.subtype,e.name) if filter.type and filter.type ~= e.type then return false end @@ -282,8 +301,8 @@ function _M:checkFilter(e, filter, type) for i = 1, #filter.not_properties do if e[filter.not_properties[i]] then return false end end end if e.checkFilter and not e:checkFilter(filter) then return false end - if filter.special and not filter.special(e) then return false end - if self.check_filter and not self:check_filter(e, filter, type) then return false end + if filter.special and not filter.special(e, filter) then return false end + if self.check_filter and not self:check_filter(e, filter, typ) then return false end if filter.max_ood and resolvers.current_level and e.level_range and resolvers.current_level + filter.max_ood < e.level_range[1] then print("Refused max_ood", e.name, e.level_range[1]) return false end if e.unique then print("accepted unique", e.name, e.__CLASSNAME.."/"..e.unique) end @@ -291,18 +310,14 @@ function _M:checkFilter(e, filter, type) return true end ---- Return a string describing the filter -function _M:filterToString(filter) - local ps = "" - for what, check in pairs(filter) do - ps = ps .. what.."="..check.."," - end - return ps +--- Return a string summary of a filter +function _M:filterToString(filter, ...) + return string.fromTable(filter, ...) end --- Picks an entity from a computed probability list function _M:pickEntity(list) - if #list == 0 then return nil end + if not list or #list == 0 then return nil end local r = rng.range(1, list.total) for i = 1, #list do -- print("test", r, ":=:", list[i].genprob) @@ -351,10 +366,12 @@ function _M:getEntities(level, type) return list end ---- Picks and resolve an entity +--- Picks and resolves an entity -- @param level a Level object to generate for --- @param type one of "object" "terrain" "actor" "trap" --- @param filter a filter table +-- @param type one of "object" "terrain" "actor" "trap" or a table of entities with __real_type defined +-- @param filter a filter table with optional fields: +-- base_list: an entities list (table) or a specifier to load the entities list from a file, format: <classname>:<file path> +-- nb_tries: maximum number of attempts to randomly pick the entity from the list -- @param force_level if not nil forces the current level for resolvers to this one -- @param prob_filter if true a new probability list based on this filter will be generated, ensuring to find objects better but at a slightly slower cost (maybe) -- @return[1] nil if a filter was given an nothing found @@ -367,10 +384,15 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter) if filter == nil then filter = util.getval(self.default_filter, self, level, type) end if filter and self.alter_filter then filter = util.getval(self.alter_filter, self, level, type, filter) end + local list + if _G.type(type) == "table" then -- use the provided list + list = type + type = type.__real_type or "" + end local e -- No probability list, use the default one and apply filter if not prob_filter then - local list = self:getEntities(level, type) + list = list or self:getEntities(level, type) local tries = filter and filter.nb_tries or 500 -- CRUDE ! Brute force ! Make me smarter ! while tries > 0 do @@ -393,14 +415,15 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter) elseif type == "actor" then base_list = self.npc_list elseif type == "object" then base_list = self.object_list elseif type == "trap" then base_list = self.trap_list - else base_list = self:getEntities(level, type) if not base_list then return nil end end + else + base_list = list or self:getEntities(level, type) if not base_list then return nil end + end local list = self:computeRarities(type, base_list, level, function(e) return self:checkFilter(e, filter, type) end, filter.add_levels, filter.special_rarity) e = self:pickEntity(list) print("[MAKE ENTITY] prob list generation", e and e.name, "from list size", #list) if not e then return nil end - -- No filter - else - local list = self:getEntities(level, type) + else -- No filter + list = list or self:getEntities(level, type) local tries = filter and filter.nb_tries or 50 -- A little crude here too but we only check 50 times, this is simply to prevent duplicate uniques while tries > 0 do e = self:pickEntity(list) @@ -420,9 +443,20 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter) return e end ---- Find a given entity and resolve it --- @return[1] nil if a filter was given an nothing found +--- Find a specific entity and resolve it +-- @param level a Level object to generate for +-- @param type defines where to find the entity definition: +-- a table is searched directly as a list of entities +-- "object" -- look in zone.object_list +-- "terrain", "grid", "trigger" -- look in zone.grid_list +-- "actor" -- look in zone.npc_list +-- "trap" -- look in zone.trap_list +-- other strings -- specifier to load the entities list from a file, format: <classname>:<file path> +-- @param name the name of the entity to find (must match entity.define_as exactly) +-- @param force_unique if not set, duplicate uniques will not be generated +-- @return[1] nil if the entity could not be found or was a pre-existing unique -- @return[2] the fully resolved entity, ready to be used on a level +-- @return[2] boolean true if the entity was a pre-existing unique function _M:makeEntityByName(level, type, name, force_unique) resolvers.current_level = self.base_level + level.level - 1 @@ -432,7 +466,16 @@ function _M:makeEntityByName(level, type, name, force_unique) elseif type == "object" then e = self.object_list[name] elseif type == "grid" or type == "terrain" or type == "trigger" then e = self.grid_list[name] elseif type == "trap" then e = self.trap_list[name] + else + local base_list + local _, _, class, file = type:find("(.*):(.*)") + if class and file then + base_list = require(class):loadList(file) + type = base_list.__real_type or type + end + e = base_list and base_list[name] end + if not e then return nil end local forced = false @@ -679,29 +722,31 @@ function _M:finishEntity(level, type, e, ego_filter) end --- Do the various stuff needed to setup an entity on the level --- Grids do not really need that, this is mostly done for traps, objects and actors<br/> +-- Grids do not really need this, it is mostly done for traps, objects and actors<br/> -- This will do all the correct initializations and setup required --- @param level the level on which to add the entity --- @param e the entity to add --- @param typ the type of entity, one of "actor", "object", "trap" or "terrain" --- @param x the coordinates where to add it. This CAN be null in which case it wont be added to the map --- @param y the coordinates where to add it. This CAN be null in which case it wont be added to the map --- @param no_added have we added it +-- @param level: the level on which to add the entity +-- @param e: the entity to add +-- @param typ: the type of entity, one of "actor", "projectile", "object", "trap", "terrain", "grid", "trigger" +-- @param x: x coordinate. This CAN be null in which case it wont be added to the map +-- @param y: y coordinate. This CAN be null in which case it wont be added to the map +-- @param no_added: set true to prevent calling e:added() +-- checks e.addedToLevel then e.on_added after adding the entity function _M:addEntity(level, e, typ, x, y, no_added) if typ == "actor" then - -- We are additing it, this means there is no old position + -- We are adding it, this means there is no old position e.x = nil e.y = nil if x and y then e:move(x, y, true) end level:addEntity(e, nil, true) if not no_added then e:added() end -- Levelup ? - if self.actor_adjust_level and e.forceLevelup then + if self.actor_adjust_level and e.forceLevelup and not e._actor_adjust_level_applied then local newlevel = self:actor_adjust_level(level, e) e:forceLevelup(newlevel + (e.__forced_level or 0)) + e._actor_adjust_level_applied = true end elseif typ == "projectile" then - -- We are additing it, this means there is no old position + -- We are adding it, this means there is no old position e.x = nil e.y = nil if x and y then e:move(x, y, true) end @@ -1107,6 +1152,9 @@ function _M:newLevel(level_data, lev, old_lev, game) -- Delete the room_map if it's no longer needed if not self._retain_level_room_map then map.room_map = nil end + -- Call a "post" finisher + if level_data.post_process_end then level_data.post_process_end(level, self) end + return level end diff --git a/game/engines/default/engine/class.lua b/game/engines/default/engine/class.lua index f0c1e07fb29498adfaa3a7bd7845da93436310cb..0cef61b3dee67790e3e56635ec1d5fedd9f4bdfd 100644 --- a/game/engines/default/engine/class.lua +++ b/game/engines/default/engine/class.lua @@ -265,13 +265,13 @@ end --- Clones the object, and all subobjects without cloning a subobject twice -- @param[type=table] self Object to be cloned. --- @param[type=table] post_copy Optional, a table to be merged with the new object after cloning. +-- @param[type=table] post_copy Optional, a table to be merged (recursively) with the new object after cloning. -- @return a reference to the clone function _M:cloneFull(post_copy) local clonetable = {} local n = clonerecursfull(clonetable, self, nil, nil) if post_copy then - for k, e in pairs(post_copy) do n[k] = e end + table.merge(n, post_copy, true) end return n -- return core.serial.cloneFull(self) @@ -340,13 +340,13 @@ end -- @ or nil to use the default name/ref as keys on the clone -- @ v = the value to assign for instances of this node, -- @ or nil to use the default assignment value --- @param[type=table] post_copy Optional, a table to be merged with the new object after cloning. +-- @param[type=table] post_copy Optional, a table to be merged (recursively) with the new object after cloning. -- @return a reference to the clone function _M:cloneCustom(alt_nodes, post_copy) local clonetable = {} local n = cloneCustomRecurs(clonetable, self, nil, nil, alt_nodes) if post_copy then - for k, e in pairs(post_copy) do n[k] = e end + table.merge(n, post_copy, true) end return n end diff --git a/game/engines/default/engine/dialogs/ChatFilter.lua b/game/engines/default/engine/dialogs/ChatFilter.lua index ae60fc017568f46c15df85bf50464237660e31d3..be5d77615bee5d91263185bf0eb6463ba26cc4c7 100644 --- a/game/engines/default/engine/dialogs/ChatFilter.lua +++ b/game/engines/default/engine/dialogs/ChatFilter.lua @@ -38,6 +38,7 @@ function _M:init(adds) {name = "Other achievements", kind = "achievement_other"}, } for i, l in ipairs(adds or {}) do list[#list+1] = l end + self:triggerHook{"ChatFilters:list", list=list} local c_desc = Textzone.new{width=self.iw - 10, height=1, auto_height=true, text="Select which types of chat events to see or not."} local uis = { {left=0, top=0, ui=c_desc} } diff --git a/game/engines/default/engine/generator/map/Roomer.lua b/game/engines/default/engine/generator/map/Roomer.lua index 6b6dc2515d09ace6cbeb051a645274d81f7e6e66..adbae0493ca2b293b6134d783f806266afddffda 100644 --- a/game/engines/default/engine/generator/map/Roomer.lua +++ b/game/engines/default/engine/generator/map/Roomer.lua @@ -191,7 +191,6 @@ function _M:generate(lev, old_lev) break end end - local r = self:roomAlloc(rroom, #rooms+1, lev, old_lev) if r then nb_room = nb_room -1 end tries = tries - 1 diff --git a/game/engines/default/engine/interface/ActorInventory.lua b/game/engines/default/engine/interface/ActorInventory.lua index 8beabd78fefe1a3e084dbda340b78631fefa04eb..26c4a4267fe1349c59c1044be22c68ad069a32a0 100644 --- a/game/engines/default/engine/interface/ActorInventory.lua +++ b/game/engines/default/engine/interface/ActorInventory.lua @@ -66,20 +66,22 @@ function _M:init(t) self:initBody() end ---- generate inventories according to the body definition table +--- Generate inventories according to the body definition table +-- This creates new inventories or updates existing ones -- @param self.body = {SLOT_ID = max, ...} -- @param max = number of slots if number or table of properties (max = , stack_limit = , ..) merged into definition function _M:initBody() if self.body then - local def + local long_name, def for inven, max in pairs(self.body) do - def = self.inven_def[self["INVEN_"..inven]] - assert(def, "inventory slot undefined") - self.inven[self["INVEN_"..inven]] = {worn=def.is_worn, id=self["INVEN_"..inven], name=inven, stack_limit = def.stack_limit} + long_name = "INVEN_"..inven + def = self.inven_def[self[long_name]] + assert(def, "inventory slot undefined: "..inven) + self.inven[self[long_name]] = table.merge(self.inven[self[long_name]] or {}, {worn=def.is_worn, id=self[long_name], name=inven, short_name=def.short_name, stack_limit = def.stack_limit}) if type(max) == "table" then - table.merge(self.inven[self["INVEN_"..inven]], max, true) + table.merge(self.inven[self[long_name]], max, true) else - self.inven[self["INVEN_"..inven]].max = max + self.inven[self[long_name]].max = max end end self.body = nil @@ -97,7 +99,7 @@ function _M:getInven(id) end end ---- Returns the content of an inventory as a table +--- Returns the inventory definition function _M:getInvenDef(id) if type(id) == "number" then return self.inven_def[id] diff --git a/game/engines/default/engine/interface/ActorResource.lua b/game/engines/default/engine/interface/ActorResource.lua index 5876f1c131a26fe1e81be42f917e917527f0328a..d39574d9cb631bcb9fd2d6d102579e68ad998f2f 100644 --- a/game/engines/default/engine/interface/ActorResource.lua +++ b/game/engines/default/engine/interface/ActorResource.lua @@ -53,6 +53,7 @@ function _M:defineResource(name, short_name, talent, regen_prop, desc, min, max, talent = talent, regen_prop = regen_prop, invert_values = false, -- resource value decreases as it is consumed by default + switch_direction = false, -- resource value prefers to go to the min instead of max description = desc, minname = minname, maxname = maxname, @@ -126,7 +127,7 @@ function _M:init(t) for i, r in ipairs(_M.resources_def) do self[r.minname] = t[r.minname] or r.min self[r.maxname] = t[r.maxname] or r.max - self[r.short_name] = t[r.short_name] or self[r.maxname] + self[r.short_name] = t[r.short_name] or (r.switch_direction and self[r.minname] or self[r.maxname]) if r.regen_prop then self[r.regen_prop] = t[r.regen_prop] or 0 end diff --git a/game/engines/default/engine/interface/ActorTalents.lua b/game/engines/default/engine/interface/ActorTalents.lua index 02b6b339d2cc37001910108a81fbbdc937d0648a..5b21f8f95bd651835d02811b63adebf3b1ab4542 100644 --- a/game/engines/default/engine/interface/ActorTalents.lua +++ b/game/engines/default/engine/interface/ActorTalents.lua @@ -815,6 +815,9 @@ function _M:updateTalentTypeMastery(tt) end end end + if self.talents_types_def[tt] and self.talents_types_def[tt].on_mastery_change then + self.talents_types_def[tt].on_mastery_change(self, self:getTalentTypeMastery(tt), tt) + end end --- Return talent definition from id diff --git a/game/engines/default/engine/resolvers.lua b/game/engines/default/engine/resolvers.lua index f3b6767ecb008080bbc93b8a8a61e8bb0d0c0be6..03112ead69086ef260779d4e41d7a867adba4daa 100644 --- a/game/engines/default/engine/resolvers.lua +++ b/game/engines/default/engine/resolvers.lua @@ -19,6 +19,14 @@ --- @script engine.resolvers +--- resolvers are 2-part functions used to turn prototype Entities into finished versions for use in the game: +-- resolver.foo(...) (called when creating the Entity prototype) assigns a resolver table to a property field within the entity +-- The resolver table contains .__resolver == "foo" and all other data needed to create the final value for the property field when the final entity instance is prepared (performed by Entity:resolve) +--- If the resolver table contains .__resolve_instant, it will be resolved before other resolvers. +-- resolver.calc.foo(t, e) (called by Entity:resolve) calculates the final value for ("resolves") the property field where: +-- t = table generated by resolver.foo and stored in the property field +-- e = the entity on which to perform the changes +-- Entity:resolve assigns the return value of this function to the property field resolvers = {} resolvers.calc = {} diff --git a/game/engines/default/engine/ui/Dialog.lua b/game/engines/default/engine/ui/Dialog.lua index db4b0588d059da6516e11c4a116441ada5e431d6..961193a1c38a19b31d37e2bdb082c18d48b01483 100644 --- a/game/engines/default/engine/ui/Dialog.lua +++ b/game/engines/default/engine/ui/Dialog.lua @@ -243,8 +243,10 @@ end -- @param w, h = width and height of the dialog (in pixels, optional: dialog sized to its elements by default) -- @param no_leave set true to force a selection -- @param escape = the default choice (number) to select if escape is pressed -function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leave, escape) +-- @param default = the default choice (number) to select (highlight) when the dialog opens, default 1 +function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leave, escape, default) escape = escape or 1 + default = default or 1 -- compute display limits local max_w, max_h = w or game.w*.75, h or game.h*.75 @@ -256,7 +258,10 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav local d = new(title, w or 1, h or 1) --print(("[multiButtonPopup] initialized: (w:%s,h:%s), (maxw:%s,maxh:%s) "):format(w, h, max_w, max_h)) - if not no_leave then d.key:addBind("EXIT", function() game:unregisterDialog(d) game:unregisterDialog(d) choice_fct(button_list[escape]) end) end + if not no_leave then d.key:addBind("EXIT", function() game:unregisterDialog(d) + if choice_fct then choice_fct(button_list[escape]) end + end) + end local num_buttons = math.min(#button_list, 50) local buttons, buttons_width, button_height = {}, 0, 0 @@ -298,7 +303,7 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav local width = w or math.min(max_w, math.max(text_width + 20, max_buttons_width + 20)) local height = h or math.min(max_h, text_height + 10 + nrow*button_height) local uis = { - {left = (width - text_width)/2, top = 3, ui=require("engine.ui.Textzone").new{width=text_width, height=text_height, text=text}} + {left = (width - text_width)/2, top = 3, ui=require("engine.ui.Textzone").new{width=text_width, height=text_height, text=text, can_focus=false}} } -- actually place the buttons in the dialog top = math.max(text_height, text_height + (height - text_height - nrow*button_height - 5)/2) @@ -312,7 +317,10 @@ function _M:multiButtonPopup(title, text, button_list, w, h, choice_fct, no_leav end end d:loadUI(uis) - if uis[escape + 1] then d:setFocus(uis[escape + 1]) end + -- set default focus if possible + if uis[default + 1] then d:setFocus(uis[default + 1]) + elseif uis[escape + 1] then d:setFocus(uis[escape + 1]) + end d:setupUI(not w, not h) game:registerDialog(d) return d diff --git a/game/engines/default/engine/ui/Textbox.lua b/game/engines/default/engine/ui/Textbox.lua index 2ba2115ef2c9719f7fc0cbdb5970e3078d781d79..bcfcc75e0016458c81160daa4c1a7b9996a2a2b5 100644 --- a/game/engines/default/engine/ui/Textbox.lua +++ b/game/engines/default/engine/ui/Textbox.lua @@ -98,17 +98,23 @@ function _M:generate() self.key:addCommands{ _LEFT = function() self.cursor = util.bound(self.cursor - 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end, _RIGHT = function() self.cursor = util.bound(self.cursor + 1, 1, #self.tmp+1) self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) self:updateText() end, - _DELETE = function() - if self.cursor <= #self.tmp then - table.remove(self.tmp, self.cursor) + _BACKSPACE = function() + if self.cursor > 1 then + local st = core.key.modState("ctrl") and 1 or self.cursor - 1 + for i = self.cursor - 1, st, -1 do + table.remove(self.tmp, i) + self.cursor = self.cursor - 1 + self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) + end self:updateText() end end, - _BACKSPACE = function() - if self.cursor > 1 then - table.remove(self.tmp, self.cursor - 1) - self.cursor = self.cursor - 1 - self.scroll = util.scroll(self.cursor, self.scroll, self.max_display) + _DELETE = function() + if self.cursor <= #self.tmp then + local num = core.key.modState("ctrl") and #self.tmp - self.cursor + 1 or 1 + for i = 1, num do + table.remove(self.tmp, self.cursor) + end self:updateText() end end, @@ -145,6 +151,10 @@ function _M:generate() self:updateText() end end, + [{"_c", "ctrl"}] = function(c) + self:updateText() + core.key.setClipboard(self.text) + end, } end diff --git a/game/engines/default/engine/utils.lua b/game/engines/default/engine/utils.lua index ae728c14f2162390830c00fe98396cb06f7c311f..7678f53900e54c034e2c5d51a0fb9066c320f81d 100644 --- a/game/engines/default/engine/utils.lua +++ b/game/engines/default/engine/utils.lua @@ -158,7 +158,7 @@ end --- Returns a clone of a table -- @param tbl The original table to be cloned --- @param deep Boolean to determine if recursive cloning occurs +-- @param deep Boolean allow recursive cloning (unless .__ATOMIC or .__CLASSNAME is defined) -- @param k_skip A table containing key values set to true if you want to skip them. -- @return The cloned table. function table.clone(tbl, deep, k_skip) @@ -181,27 +181,27 @@ end table.NIL_MERGE = {} --- Merges two tables in-place. --- The table.NIL_MERGE is a special value that will nil out the corresponding dst key. -- @param dst The destination table, which will have all merged values. -- @param src The source table, supplying values to be merged. -- @param deep Boolean that determines if tables will be recursively merged. -- @param k_skip A table containing key values set to true if you want to skip them. -- @param k_skip_deep Like k_skip, except this table is passed on to the deep recursions. -- @param addnumbers Boolean that determines if two numbers will be added rather than replaced. +-- assign the special value table.NIL_MERGE in src to set the corresponding dst field to nil. +-- subtables containing .__ATOMIC or .__CLASSNAME will be copied by reference. function table.merge(dst, src, deep, k_skip, k_skip_deep, addnumbers) k_skip = k_skip or {} k_skip_deep = k_skip_deep or {} for k, e in pairs(src) do if not k_skip[k] and not k_skip_deep[k] then -- Recursively merge tables - if deep and dst[k] and type(e) == "table" and type(dst[k]) == "table" and not e.__ATOMIC and not e.__CLASSNAME then + if e == table.NIL_MERGE then -- remove corresponding field + dst[k] = nil + elseif deep and dst[k] and type(e) == "table" and type(dst[k]) == "table" and not e.__ATOMIC and not e.__CLASSNAME then table.merge(dst[k], e, deep, nil, k_skip_deep, addnumbers) -- Clone tables if into the destination elseif deep and not dst[k] and type(e) == "table" and not e.__ATOMIC and not e.__CLASSNAME then dst[k] = table.clone(e, deep, nil, k_skip_deep) - -- Nil out any NIL_MERGE entries - elseif e == table.NIL_MERGE then - dst[k] = nil -- Add number entries if "add" is set elseif addnumbers and not dst.__no_merge_add and dst[k] and type(dst[k]) == "number" and type(e) == "number" then dst[k] = dst[k] + e @@ -814,6 +814,92 @@ function string.splitLines(str, max_width, font) return lines end +--- create a textual abbreviation for a function +-- @param fct the function +-- @param fmt output format for filepath, line number, first line of code +-- (default: "\"<function( defined: %s, line %s): %s>\"" ) +-- @return string using the format provided +function string.fromFunction(fct, fmt) + local info = debug.getinfo(fct, "S") + local fpath = string.gsub(info.source,"@","") + local firstline = "" + fmt = fmt or "\"<function( defined: %s, line %s): %s>\"" + if not fs.exists(fpath) then + fpath = "no file path" + firstline = info.short_src + else + local f = fs.open(fpath, "r") + local line_num = 0 + while true do -- could continue with body here + firstline = f:readLine() + if firstline then + line_num = line_num + 1 + if line_num == info.linedefined then break end + end + end + end + return (fmt):format(fpath, info.linedefined, tostring(firstline)) +end + +--- Create a textual representation of a value +-- similar to tostring, but includes special handling of tables and functions +-- surrounds non-numbers/booleans/nils/functions with "" +-- @param v: the value +-- @param recurse: the recursion level for string.fromTable, set < 0 for basic tostring +-- @param offset, prefix, suffix: inputs to string.fromTable for converting tables +function string.fromValue(v, recurse, offset, prefix, suffix) + recurse, offset, prefix, suffix = recurse or 0, offset or ", ", prefix or "{", suffix or "}" + local vt, vs = type(v) + if vt == "table" then + if recurse < 0 then vs = tostring(v) + elseif v.__ATOMIC or v.__CLASSNAME then -- create entity/atomic label + local abv = {} + if v.__CLASSNAME then abv[#abv+1] = "__CLASSNAME="..tostring(v.__CLASSNAME) end + if v.__ATOMIC then abv[#abv+1] = "ATOMIC" end + vs = ("%s\"%s%s%s\"%s"):format(prefix, v, v.__CLASSNAME and ", __CLASSNAME="..tostring(v.__CLASSNAME) or "", v.__ATOMIC and ", ATOMIC" or "", suffix) + elseif recurse > 0 then -- get recursive string + vs = string.fromTable(v, recurse - 1, offset, prefix, suffix) + else vs = prefix.."\""..tostring(v).."\""..suffix + end + elseif vt == "function" then + vs = recurse >= 0 and string.fromFunction(v) or tostring(v) + elseif not (vt == "number" or vt == "boolean" or vt == "nil") then + vs = "\""..tostring(v).."\"" + end + return vs or tostring(v) +end + +--- Create a textual representation of a table +-- This is like reverse-interpreting back to lua code, compatible for strings, numbers, and tables +-- @param src: source table +-- @param recurse: recursion level for subtables (default 0) +-- @param offset: string to insert between table fields (default: ", ") +-- @param prefix: prefix for the table and subtables (default: "{") +-- @param suffix: suffix for the table and subtables (default: "}") +-- @param key_recurse the recursion level for keys that are tables (default 0) +-- @return[1] single line text representation of src +-- non-string table.keys are surrounded by "[", "]" +-- @return[2] indexed table containing strings for each key/value pair in src (@recursion level 0) +-- recursed subtables are converted and embedded +-- subtables containing .__ATOMIC or .__CLASSNAME are never converted, but are noted +-- functions are converted to embedded strings using string.fromFunction +function string.fromTable(src, recurse, offset, prefix, suffix, key_recurse) + if type(src) ~= "table" then print("string.fromTable has no table:", src) return tostring(src) end + local tt = {} + recurse, offset, prefix, suffix = recurse or 0, offset or ", ", prefix or "{", suffix or "}" + for k, v in pairs(src) do + local kt, vt = type(k), type(v) + local ks, vs + if kt ~= "string" then + ks = "["..string.fromValue(k, key_recurse, offset, prefix, suffix).."]" + end + vs = string.fromValue(v, recurse, offset, prefix, suffix) + tt[#tt+1] = ("%s=%s"):format(ks or tostring(k), vs or tostring(v)) + end + -- could sort here if desired + return prefix..table.concat(tt, offset)..suffix, tt +end + -- Split a string by the given character(s) function string.split(str, char, keep_separator) char = lpeg.P(char) diff --git a/game/engines/default/modules/boot/dialogs/ProfileLogin.lua b/game/engines/default/modules/boot/dialogs/ProfileLogin.lua index 68d09d7217e57f54d153128cd27c69e680abad4e..a4c8dea90b8eccfdcca7dc58916be2e2b5872698 100644 --- a/game/engines/default/modules/boot/dialogs/ProfileLogin.lua +++ b/game/engines/default/modules/boot/dialogs/ProfileLogin.lua @@ -110,7 +110,7 @@ function _M:okclick() end game:unregisterDialog(self) - game:createProfile({create=self.c_email and true or false, login=self.c_login.text, pass=self.c_pass.text, email=self.c_email and self.c_email.text, news=self.c_news.checked}) + game:createProfile({create=self.c_email and true or false, login=self.c_login.text, pass=self.c_pass.text, email=self.c_email and self.c_email.text, news=self.c_news and self.c_news.checked}) end function _M:cancelclick() diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua index efe57059b53e15fd1fa246f5eaf8da21d3b036b8..8379f5cf1166ae8de8b949d45ab6afd1ef57016e 100644 --- a/game/modules/tome/class/Actor.lua +++ b/game/modules/tome/class/Actor.lua @@ -74,6 +74,25 @@ _M._no_save_fields.resting = true -- No need to save __project_source either, it's a turn by turn thing _M._no_save_fields.__project_source = true +-- alt_node fields (controls fields copied with cloneActor by default) +_M.clone_nodes = table.merge({running_fov=false, running_prev=false, + -- spawning/death fields: + make_escort=false, escort_quest=false, summon=false, on_added_to_level=false, on_added=false, clone_on_hit=false, on_die=false, die=false, self_ressurect=false, + -- AI fields: + on_acquire_target=false, seen_by=false, + -- NPC interaction: + on_take_hit=false, can_talk=false, + -- player fields: + puuid=false, quests=false, random_escort_levels=false, achievements=false, achievement_data=false, game_ender=false, + last_learnt_talents = false, died=false, died_times=false, killedBy=false, all_kills=false, all_kills_kind=false, +},_M.clone_nodes, true) + +--- cloneActor default post copy fields (merged by cloneActor) +_M.clone_copy = table.merge({no_drops=true, no_rod_recall=true, no_inventory_access=true, no_levelup_access=true, + remove_from_party_on_death=true, keep_inventory_on_death=false, + energy={value=0}, + }, _M.clone_copy or {}) + -- Use distance maps _M.__do_distance_map = true @@ -1648,6 +1667,7 @@ function _M:reactionToward(target, no_reflection) if rsrc == target and self ~= target and target:attr("encased_in_ice") then return -50 end -- summons shouldn't hate each other and shouldn't hate summoner more than enemies -- Neverending hatred + if rtarget:attr("hated_by_everybody") and rtarget ~= rsrc then return -100 end if rsrc:attr("hates_everybody") and rtarget ~= rsrc then return -100 end if rsrc:attr("hates_arcane") and rtarget:attr("has_arcane_knowledge") and not rtarget:attr("forbid_arcane") then return -100 end if rsrc:attr("hates_antimagic") and rtarget:attr("forbid_arcane") then return -100 end @@ -2837,7 +2857,6 @@ function _M:takeHit(value, src, death_note) end end - local dead, val = mod.class.interface.ActorLife.takeHit(self, value, src, death_note) if src and src.fireTalentCheck then src:fireTalentCheck("callbackOnDealDamage", val, self, dead, death_note) end @@ -2854,6 +2873,7 @@ function _M:takeHit(value, src, death_note) return dead, val end +--- Remove certain effects when cloned function _M:removeTimedEffectsOnClone() local todel = {} for eff, p in pairs(self.tmp) do @@ -2864,6 +2884,17 @@ function _M:removeTimedEffectsOnClone() while #todel > 0 do self:removeEffect(table.remove(todel)) end end +--- Unlearn certain talents when cloned +function _M:unlearnTalentsOnClone() + local todel = {} + for tid, lev in pairs(self.talents) do + if _M.talents_def[tid].unlearn_on_clone then + todel[#todel+1] = tid + end + end + while #todel > 0 do self:unlearnTalentFull(table.remove(todel)) end +end + function _M:resolveSource() if self.summoner_gain_exp and self.summoner then return self.summoner:resolveSource() @@ -3281,7 +3312,7 @@ function _M:resetToFull() self.mana = self:getMaxMana() end else - if res_def.invert_values then + if res_def.invert_values or res_def.switch_direction then self[res_def.short_name] = self:check(res_def.getMinFunction) or self[res_def.short_name] or res_def.min else self[res_def.short_name] = self:check(res_def.getMaxFunction) or self[res_def.short_name] or res_def.max @@ -3292,6 +3323,15 @@ end -- Level up talents to match actor level function _M:resolveLevelTalents() + -- learn any talents previously set to level up + if self.learn_tids then + for tid, lev in pairs(self.learn_tids) do + if self:getTalentLevelRaw(tid) < lev then + self:learnTalent(tid, true, lev - self:getTalentLevelRaw(tid)) + end + end + self.learn_tids = nil + 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 @@ -4199,7 +4239,7 @@ end --- Checks if the given item should respect its slot_forbid value -- @param o the item to check --- @param in_inven the inventory id in which the item is worn or tries to be worn +-- @param in_inven_id the inventory id in which the item is worn or tries to be worn function _M:slotForbidCheck(o, in_inven_id) in_inven_id = self:getInven(in_inven_id).id if self:attr("allow_mainhand_2h_in_1h") and in_inven_id == self.INVEN_MAINHAND and o.slot_forbid == "OFFHAND" then @@ -4208,59 +4248,119 @@ function _M:slotForbidCheck(o, in_inven_id) return true end ---- Can we wear this item? -function _M:canWearObject(o, try_slot) - if self:attr("forbid_arcane") and o.power_source and o.power_source.arcane then - return nil, "antimagic" +--- Try to wear objects carried in main inventory, looking for the best spot to wear each +-- @param force: force retrying wearing of equipment (if new objects have been added) +-- @param ... arguments passed to obj:wornLocations for each object (weight_fn, filter_field, no_type_check) +-- generally, an object is only replaced if the replacement passes the defined filter or matches its type/subtype +function _M:wearAllInventory(force, ...) + local MainInven, o = self:getInven(self.INVEN_INVEN) + if MainInven and (force or not MainInven._no_equip_objects) then + for i = #MainInven, 1, -1 do + if not o then print("[Actor:wearAllInventory]", self.uid, self.name, "wearing main inventory") end + o = MainInven[i] + --print("[Actor:wearAllInventory]", self.uid, self.name, "checking", o.name, "type", o.type, o.subtype) + if game.state:checkPowers(self, o, nil, "antimagic_only") then -- check antimagic restrictions + local locs = o:wornLocations(self, ...) -- find places to wear + -- Note: won't remove slot_forbid items or auto-swap tinkers (Actor:doWear) + if locs then + o = self:removeObject(self.INVEN_INVEN, i) -- remove from main inventory + for j, inv in ipairs(locs) do + local ro, worn = inv.inv[inv.slot] + --print("\t\t attempting to wear", o.uid, o.name, "in", inv.inv.name, inv.slot) + worn = self:wearObject(o, true, false, inv.inv, inv.slot) + if worn then + print("[Actor:wearAllInventory]", self.name, self.uid, o.uid, o.name, "WORN IN", inv.inv.name, inv.slot) + if type(worn) == "table" then + print(" --- replaced:", worn.uid, worn.name) + self:addObject(self.INVEN_INVEN, worn) + end + break + end + end + -- return to main inventory if not worn + if not o.wielded then self:addObject(self.INVEN_INVEN, o) end + end + end + end + MainInven._no_equip_objects = true end --- if o.power_source and o.power_source.antimagic and not self:attr("forbid_arcane") then --- return nil, "requires antimagic" --- end +end - local oldreq = nil +--- Get updated requirements to wear an object +-- applies known talents to modify requirements to wear an object +-- @param o the object to test +-- @return the new object require table +-- @return the old object require table +function _M:updateObjectRequirements(o) + local oldreq = rawget(o, "require") + if not oldreq then return oldreq end + local newreq if o.subtype == "shield" and self:knowTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE) then - oldreq = rawget(o, "require") - o.require = table.clone(oldreq or {}, true) - if o.require.stat and o.require.stat.str then - o.require.stat.cun, o.require.stat.str = o.require.stat.str, nil + newreq = newreq or table.clone(oldreq, true) + if newreq.stat and newreq.stat.str then + newreq.stat.cun, newreq.stat.str = newreq.stat.str, nil end - if o.require.talent then for i, tr in ipairs(o.require.talent) do + if newreq.talent then for i, tr in ipairs(newreq.talent) do if tr[1] == self.T_ARMOUR_TRAINING then - o.require.talent[i] = {self.T_SKIRMISHER_BUCKLER_EXPERTISE, 1} + newreq.talent[i] = {self.T_SKIRMISHER_BUCKLER_EXPERTISE, 1} break end end end end if o.subtype == "shield" and self:knowTalent(self.T_AGILE_DEFENSE) then - oldreq = rawget(o, "require") - o.require = table.clone(oldreq or {}, true) - if o.require.stat and o.require.stat.str then - o.require.stat.dex, o.require.stat.str = o.require.stat.str, nil + newreq = newreq or table.clone(oldreq, true) + if newreq.stat and newreq.stat.str then + newreq.stat.dex, newreq.stat.str = newreq.stat.str, nil end - if o.require.talent then for i, tr in ipairs(o.require.talent) do + if newreq.talent then for i, tr in ipairs(newreq.talent) do if tr[1] == self.T_ARMOUR_TRAINING then - o.require.talent[i] = {self.T_AGILE_DEFENSE, 1} + newreq.talent[i] = {self.T_AGILE_DEFENSE, 1} break end end end end if (o.type == "weapon" or o.type == "ammo") and self:knowTalent(self.T_STRENGTH_OF_PURPOSE) then - oldreq = rawget(o, "require") - o.require = table.clone(oldreq or {}, true) - if o.require.stat and o.require.stat.str then - o.require.stat.mag, o.require.stat.str = o.require.stat.str, nil + newreq = newreq or table.clone(oldreq, true) + if newreq.stat and newreq.stat.str then + newreq.stat.mag, newreq.stat.str = newreq.stat.str, nil end end + return newreq or oldreq, oldreq +end - local ok, reason = engine.interface.ActorInventory.canWearObject(self, o, try_slot) - - if oldreq then - o.require = oldreq +--- Can we wear this item? +function _M:canWearObject(o, try_slot) + if self:attr("forbid_arcane") and o.power_source and o.power_source.arcane then + return nil, "antimagic" end + local oldreq + o.require, oldreq = self:updateObjectRequirements(o) + + local ok, reason = engine.interface.ActorInventory.canWearObject(self, o, try_slot) + o.require = oldreq return ok, reason end +--- search all inventories for an object (including attached tinkers) +-- @param o: the object to look for +-- @param fct: optional function(o, self, inven, slot, attached) to call when found +-- @return[1] nil if not found +-- @return[2] found inventory +-- @return[2] found inventory slot +-- @return[2] found object o was attacked to (if o.is_tinker is defined) +function _M:searchAllInventories(o, fct) + local inv, slot, attached + self:inventoryApplyAll(function (inven, item, obj) + if obj == o or o.is_tinker and obj.tinker == o then + inv, slot, attached = inven, item, obj.tinker == o and obj or nil + --print("[Actor:searchAllInventories] found", o.name, "Inven:", inv.name or inv.id, "Slot:", slot, "tinkered to:", attached and attached.name) + if fct then fct(o, self, inv, slot, attached) end + end + end) + return inv, slot, attached +end + function _M:lastLearntTalentsMax(what) if self:attr("infinite_respec") then return 99999 end return what == "generic" and 3 or 4 @@ -4781,6 +4881,10 @@ function _M:getFeedbackDecay(mult) end end +function _M:alterTalentCost(t, rname, cost) + return cost +end + --- Called before a talent is used -- Check the actor can cast it -- @param ab the talent (not the id, the table) @@ -4849,6 +4953,7 @@ function _M:preUseTalent(ab, silent, fake) cost = ab[res_def.sustain_prop] if cost then cost = util.getval(cost, self, ab) or 0 + cost = self:alterTalentCost(ab, res_def.sustain_prop, cost) rmin, rmax = self[res_def.getMinFunction](self), self[res_def.getMaxFunction](self) if cost ~= 0 and self[res_def.minname] and self[res_def.maxname] and self[res_def.minname] + cost > self[res_def.maxname] then if not silent then game.logPlayer(self, "You %s %s to activate %s.", res_def.invert_values and "have too much committed" or "do not have enough uncommitted", res_def.name, ab.name) end @@ -4878,10 +4983,11 @@ function _M:preUseTalent(ab, silent, fake) cost = ab[rname] if cost then cost = (util.getval(cost, self, ab) or 0) * (util.getval(res_def.cost_factor, self, ab, true) or 1) + cost = self:alterTalentCost(ab, rname, cost) if cost ~= 0 then rmin, rmax = self[res_def.getMinFunction](self), self[res_def.getMaxFunction](self) if res_def.invert_values then - if rmax and self[res_def.getFunction](self) + cost > rmax then -- too much + if not res_def.ignore_max_use and rmax and self[res_def.getFunction](self) + cost > rmax then -- too much if not silent then game.logPlayer(self, "You have too much %s to use %s.", res_def.name, ab.name) end self.on_preuse_checking_resources = nil return false @@ -5344,6 +5450,7 @@ function _M:postUseTalent(ab, ret, silent) cost = ab[res_def.sustain_prop] if cost then cost = (util.getval(cost, self, ab) or 0) + cost = self:alterTalentCost(ab, res_def.sustain_prop, cost) if cost ~= 0 then trigger = true ret._applied_costs[res_def.short_name] = cost @@ -5358,6 +5465,7 @@ function _M:postUseTalent(ab, ret, silent) cost = ab[res_def.drain_prop] if cost then cost = util.getval(cost, self, ab) or 0 + cost = self:alterTalentCost(ab, res_def.drain_prop, cost) if cost ~= 0 then trigger = true ret._applied_drains[res_def.short_name] = cost @@ -5442,6 +5550,7 @@ function _M:postUseTalent(ab, ret, silent) for res, res_def in ipairs(_M.resources_def) do rname = res_def.short_name cost = ab[rname] and util.getval(ab[rname], self, ab) or 0 + cost = self:alterTalentCost(ab, rname, cost) if cost ~= 0 then trigger = true cost = cost * (util.getval(res_def.cost_factor, self, ab) or 1) @@ -5686,17 +5795,20 @@ function _M:getTalentFullDescription(t, addlevel, config, fake_mastery) if not res_def.hidden_resource then -- list resource cost local cost = t[res_def.short_name] and util.getval(t[res_def.short_name], self, t) or 0 + cost = self:alterTalentCost(t, res_def.short_name, cost) if cost ~= 0 then cost = cost * (util.getval(res_def.cost_factor, self, t) or 1) - d:add({"color",0x6f,0xff,0x83}, ("%s cost: "):format(res_def.name:capitalize()), res_def.color or {"color",0xff,0xa8,0xa8}, ""..math.round(cost, .1), true) + d:add({"color",0x6f,0xff,0x83}, ("%s %s: "):format(res_def.name:capitalize(), cost >= 0 and "cost" or "gain"), res_def.color or {"color",0xff,0xa8,0xa8}, ""..math.round(math.abs(cost), .1), true) end -- list sustain cost cost = t[res_def.sustain_prop] and util.getval(t[res_def.sustain_prop], self, t) or 0 + cost = self:alterTalentCost(t, res_def.sustain_prop, cost) if cost ~= 0 then d:add({"color",0x6f,0xff,0x83}, ("Sustain %s cost: "):format(res_def.name:lower()), res_def.color or {"color",0xff,0xa8,0xa8}, ""..math.round(cost, .1), true) end -- list drain cost cost = t[res_def.drain_prop] and util.getval(t[res_def.drain_prop], self, t) or 0 + cost = self:alterTalentCost(t, res_def.drain_prop, cost) if cost ~= 0 then if res_def.invert_values then d:add({"color",0x6f,0xff,0x83}, ("%s %s: "):format(cost > 0 and "Generates" or "Removes", res_def.name:lower()), res_def.color or {"color",0xff,0xa8,0xa8}, ""..math.round(math.abs(cost), .1), true) @@ -5845,7 +5957,12 @@ function _M:startTalentCooldown(t, v) self.talents_cd[t.id] = math.max(v, self.talents_cd[t.id] or 0) else if not t.cooldown then return end - self.talents_cd[t.id] = self:getTalentCooldown(t) + local cd = self:getTalentCooldown(t) + + local hd = {"Actor:startTalentCooldown", t=t, cd=cd} + if self:triggerHook(hd) then cd = hd.cd end + + self.talents_cd[t.id] = cd if t.id ~= self.T_REDUX and self:hasEffect(self.EFF_REDUX) then local eff = self:hasEffect(self.EFF_REDUX) @@ -6381,22 +6498,32 @@ local save_for_effects = { } _M.save_for_effects = save_for_effects ---- Adjust temporary effects +--- Adjust temporary effects right before they are applied +-- @param eff_id the effect ID being applied +-- @param e the effect definition +-- @param p table containing the parameters for the effect being applied +-- @return true if the effect should NOT be set function _M:on_set_temporary_effect(eff_id, e, p) - p.getName = self.tempeffect_def[eff_id].getName - p.resolveSource = self.tempeffect_def[eff_id].resolveSource + p.getName = e.getName + p.resolveSource = e.resolveSource + + local olddur = old and not e.on_merge and old.dur or 0 -- let mergable effects handle their own duration + -- Adjust duration based on saves if p.apply_power and (save_for_effects[e.type] or p.apply_save) then - local save = 0 p.maximum = p.dur p.minimum = p.min_dur or 0 --Default minimum duration is 0. Can specify something else by putting min_dur=foo in p when calling setEffect() - save = self[p.apply_save or save_for_effects[e.type]](self) - --local duration = p.maximum - math.max(0, math.floor((save - p.apply_power) / 5)) - --local duration = p.maximum - math.max(0, (math.floor(save/5) - math.floor(p.apply_power/5))) - local percentage = 1 - ((save - p.apply_power)/20) + local save = self[p.apply_save or save_for_effects[e.type]](self) + local saved, savechance = self:checkHitOld(save, p.apply_power) -- get save and save chance + -- failed save tuning parameters: increase mean_fact to increase avg duration, std_dev for more randomness + local mean_fact, std_dev = 1.1, 50 + local mean_pct = (100-savechance)*mean_fact -- mean % duration on a failed save + local percentage = util.bound(rng.normalFloat(mean_pct, std_dev)/100, 0, 2) -- fraction duration + local desired = p.maximum * percentage local fraction = desired % 1 desired = math.floor(desired) + (rng.percent(100*fraction) and 1 or 0) local duration = math.min(p.maximum, desired) + print(("[on_set_temporary_effect] %s Save %d vs Power %d (%d%% save: %s) :: dur mult: %0.3f(%d%%) :: dur %s ==> %d"):format(self.name, save, p.apply_power, savechance, saved, percentage, mean_pct, p.dur, duration)) p.dur = util.bound(duration, p.minimum or 0, p.maximum) p.amount_decreased = p.maximum - p.dur local save_type = nil @@ -6407,17 +6534,17 @@ function _M:on_set_temporary_effect(eff_id, e, p) elseif save_type == "combatSpellResist" then p.save_string = "Spell save" end - if not p.no_ct_effect and not e.no_ct_effect and e.status == "detrimental" then self:crossTierEffect(eff_id, p.apply_power, p.apply_save or save_for_effects[e.type]) end - p.total_dur = p.dur - if p.dur > 0 and e.status == "detrimental" then - local saved = self:checkHit(save, p.apply_power, 0, 95) + -- apply cross-tier effects + if not p.no_ct_effect and not e.no_ct_effect then self:crossTierEffect(eff_id, p.apply_power, p.apply_save or save_for_effects[e.type]) end + p.total_dur = p.dur + local hd = {"Actor:effectSave", saved=saved, save=save, save_type=save_type, eff_id=eff_id, e=e, p=p,} self:triggerHook(hd) self:fireTalentCheck("callbackOnEffectSave", hd) saved, eff_id, e, p = hd.saved, hd.eff_id, hd.e, hd.p if saved then - game.logSeen(self, "#ORANGE#%s shrugs off the effect '%s'!", self.name:capitalize(), e.desc) + self:logCombat(p.src, "#ORANGE#%s shrugs off %s '%s'!", self.name:capitalize(), p.src and "#Target#'s" or "the effect", e.desc) return true end end @@ -6470,11 +6597,15 @@ function _M:on_set_temporary_effect(eff_id, e, p) self:triggerTalent(self.T_SPINE_OF_THE_WORLD) end + -- This could be utilized for more effects if self:fireTalentCheck("callbackOnTemporaryEffect", eff_id, e, p) then return true end - if self.player and not self.tmp[eff_id] then + if self.player and not old then p.__set_time = core.game.getTime() end + + if p.dur <= 0 then return true end + p.dur = math.max(p.dur, olddur) -- don't shorten the existing duration because of a new application (on_merge may change it) end function _M:on_temporary_effect_added(eff_id, e, p) @@ -6854,8 +6985,8 @@ end function _M:canUseTinker(tinker) if not tinker.is_tinker then return nil, "not an attachable item" end - if not self.can_tinker then return nil, "can not use attachements" end - if not self.can_tinker[tinker.is_tinker] then return nil, "can not use attachements of this type" end + if not self.can_tinker then return nil, "can not use attachments" end + if not self.can_tinker[tinker.is_tinker] then return nil, "can not use attachments of this type" end if tinker.tinker_allow_attach and tinker:tinker_allow_attach() then return nil, tinker:tinker_allow_attach() end return true end diff --git a/game/modules/tome/class/Game.lua b/game/modules/tome/class/Game.lua index 6d369e1e73b0676d86e3766179efd775f5ac771f..f79da91c696b6c4559b7bd276184b3caccb02220 100644 --- a/game/modules/tome/class/Game.lua +++ b/game/modules/tome/class/Game.lua @@ -944,7 +944,7 @@ function _M:changeLevelReal(lev, zone, params) end -- clear chrono worlds and their various effects - if self._chronoworlds then self._chronoworlds = nil end + if self._chronoworlds and not params.keep_chronoworlds then self._chronoworlds = nil end local left_zone = self.zone local old_lev = (self.level and not zone) and self.level.level or -1000 @@ -1857,8 +1857,6 @@ do return end print(f, err) setfenv(f, setmetatable({level=self.level, zone=self.zone}, {__index=_G})) print(pcall(f)) -do return end - self:registerDialog(require("mod.dialogs.DownloadCharball").new()) end end, [{"_f","ctrl"}] = function() if config.settings.cheat then self.player.quests["love-melinda"] = nil diff --git a/game/modules/tome/class/GameState.lua b/game/modules/tome/class/GameState.lua index 0be96c7db9ac6b7e036dcd54bad6242d8c195df7..3625c01046a57fe0f91e836eaa97398cf28bf3a1 100644 --- a/game/modules/tome/class/GameState.lua +++ b/game/modules/tome/class/GameState.lua @@ -424,9 +424,6 @@ function _M:generateRandart(data) local display = o.display ---o.baseobj = base:cloneFull() -- debugging code ---o.gendata = table.clone(data, true) -- debugging code - -- Load possible random powers local powers_list = engine.Object:loadList(o.randart_able, nil, nil, function(e) @@ -1118,7 +1115,7 @@ end -------------------------------------------------------------- -- Loot filters -------------------------------------------------------------- - +-- These are referenced by the "tome_drops" field in object filters local drop_tables = { normal = { [1] = { @@ -1293,6 +1290,7 @@ local drop_tables = { }, } +-- These are referenced by the "tome_mod" field in object filters (multipliers for drop_tables) local loot_mod = { uvault = { -- Uber vault uniques = 40, @@ -1332,6 +1330,7 @@ local loot_mod = { }, } +--- get the default drop table for the current level and zone local default_drops = function(zone, level, what) if zone.default_drops then return zone.default_drops end local lev = util.bound(math.ceil(zone:level_adjust_level(level, "object") / 10), 1, 5) @@ -1348,8 +1347,13 @@ function _M:defaultEntityFilter(zone, level, type) } end ---- Alter any entity filters to process tome specific loot tables +--- Alter any entity filters to process tome specific loot tables (Objects only) -- Here be magic! We tweak and convert and turn and create filters! It's magic but it works :) +-- Filter fields interpreted: +-- force_tome_drops: set true to use the tome default drop tables (defined above) +-- no_tome_drops: set true to prevent auto loading default tome drop tables +-- tome: specific tome drop table to use (set == true to use the default "normal" table for the zone/level) +-- tome_mod: specific table of multipliers for each type of drop or a string indexing a loot_mod table (defined above) function _M:entityFilterAlter(zone, level, type, filter) if type ~= "object" then return filter end @@ -1446,6 +1450,13 @@ function _M:entityFilterAlter(zone, level, type, filter) return filter end +--- Provide some additional filter checks to apply when generating an entity +-- called in Zone:makeEntity by the zone.check_filter function generated when loading a zone +-- filter fields interpreted: +-- ignore_material_restriction: set true to ignore zone material level restrictions (Objects) +-- tome_mod.material_mod: increase maximum allowed material level +-- forbid_power_source: table of power sources not allowed +-- power_source: table of power sources required function _M:entityFilter(zone, e, filter, type) if filter.forbid_power_source then if e.power_source then @@ -1486,6 +1497,12 @@ function _M:entityFilter(zone, e, filter, type) end end +--- make some changes to an entity based on its filter parameters before finishing (resolving) it +-- called in Zone:makeEntity by the zone.post_filter function generated when loading a zone +-- filter fields interpreted: +-- random_boss: data to convert a non-unique actor to a random boss with game.state:createRandomBoss +-- random_elite: data to merge to convert a non-unique actor to a random elite with game.state:createRandomBoss +-- random_object: data to convert a non-unique object into random_object using game.state:generateRandart function _M:entityFilterPost(zone, level, type, e, filter) if type == "actor" then if filter.random_boss and not e.unique then @@ -1570,6 +1587,9 @@ function _M:entityFilterPost(zone, level, type, e, filter) return e end +--- Modify/create an ego filter(objects only, used when adding egos) +-- called in Zone:finishEntity by the zone.ego_filter function generated when loading a zone +-- checks power_source compatibility for egos as they are added to the object function _M:egoFilter(zone, level, type, etype, e, ego_filter, egos_list, picked_etype) if type ~= "object" then return ego_filter end @@ -1904,20 +1924,24 @@ function _M:createRandomZone(zbase) return zone, boss end ---- Add character classes to an actor updating stats, talents, and equipment +--- Add one or more character classes to an actor, updating stats, talents, and equipment -- @param b = actor(boss) to update -- @param data = optional parameters: --- @param data.force_classes = specific classes to apply first {Corruptor = true, Bulwark = true, ...} ignores restrictions --- forced classes are applied first, ignoring restrictions +-- @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. -- (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 for no equipment <nil> --- @param data.loot_quality = drop table to use <"boss"> +-- @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> -- @param instant set true to force instant learning of talents and generating golem <nil> function _M:applyRandomClass(b, data, instant) @@ -1937,7 +1961,7 @@ function _M:applyRandomClass(b, data, instant) end if not mclass then return end - print("Adding to random boss class", class.name, mclass.name) + 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 {} @@ -1949,17 +1973,19 @@ function _M:applyRandomClass(b, data, instant) 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),",")) + print(" power types: not_power_source =", table.concat(table.keys(b.not_power_source),","), "power_source =", table.concat(table.keys(b.power_source),",")) - -- Add stats - if b.auto_stats then - b.stats = b.stats or {} + -- 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 - b.stats[stat] = (b.stats[stat] or 10) + v - for i = 1, v do b.auto_stats[#b.auto_stats+1] = b.stats_def[stat].id end + 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 @@ -1979,18 +2005,21 @@ print(" power types: not_power_source =", table.concat(table.keys(b.not_power_ -- Add starting equipment local apply_resolvers = function(k, resolver) if type(resolver) == "table" and resolver.__resolver then - if resolver.__resolver == "equip" and not data.forbid_equip then - resolver[1].id = nil - -- Make sure we equip some nifty stuff instead of player's starting iron stuff - for i, d in ipairs(resolver[1]) do - d.name = nil - d.ego_chance = nil - d.forbid_power_source=b.not_power_source - d.tome_drops = data.loot_quality or "boss" - d.force_drop = (data.drop_equipment == nil) and true or data.drop_equipment + if resolver.__resolver == "equip" then + if not data.forbid_equip then + resolver[1].id = nil + -- Make sure we equip some nifty stuff instead of player's starting iron stuff + for i, d in ipairs(resolver[1]) do + d.name, d.id = nil, nil + d.ego_chance = nil + d.ignore_material_restriction = true + d.forbid_power_source = b.not_power_source + d.tome_drops = data.loot_quality or "boss" + d.force_drop = (data.drop_equipment == nil) and true or data.drop_equipment + end + b[#b+1] = resolver end - b[#b+1] = resolver - elseif resolver.__resolver == "inscription" then -- add support for inscriptions + elseif resolver._allow_random_boss then -- explicitly allowed resolver b[#b+1] = resolver end elseif k == "innate_alchemy_golem" then @@ -2000,12 +2029,14 @@ print(" power types: not_power_source =", table.concat(table.keys(b.not_power_ if instant then b:check("birth_create_alchemist_golem") end elseif k == "soul" then b.soul = util.bound(1 + math.ceil(data.level / 10), 1, 10) -- Does this need to scale? + elseif k == "can_tinker" then + b[k] = table.clone(resolver) end end 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 - -- Starting talents are autoleveling + -- 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 @@ -2066,10 +2097,10 @@ print(" power types: not_power_source =", table.concat(table.keys(b.not_power_ local step = max / 50 local lev = math.ceil(step * data.level) print(count, " * talent:", tid, lev) - if instant then + 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 + 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 @@ -2079,18 +2110,40 @@ print(" power types: not_power_source =", table.concat(table.keys(b.not_power_ end end print(" ** Finished adding", count, "of", nb, "random class talents") + return true end + -- add a full set of inventories if needed + if data.update_body then + 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 } + b:initBody() + end + -- 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) + local force_classes = {} + for i, c_name in ipairs(c_list) do + force_classes[i] = c_list[i] + c_list[i] = nil + end + table.append(force_classes, table.shuffle(table.keys(c_list))) + for i, c_name in ipairs(force_classes) do + if classes[c_name] then + apply_class(table.clone(classes[c_name], true)) + else + print(" ###Forced class", c_name, "NOT DEFINED###") + end + end + end local list = {} - local force_classes = data.force_classes and table.clone(data.force_classes) for name, cdata in ipairs(classes) do - if force_classes and force_classes[cdata.name] then apply_class(table.clone(cdata, true)) force_classes[cdata.name] = nil - elseif 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 + 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 end end + local to_apply = data.nb_classes or 2 while to_apply > 0 do local c = rng.tableRemove(list) @@ -2098,9 +2151,15 @@ print(" power types: not_power_source =", table.concat(table.keys(b.not_power_ 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 else - print(" class", c.name, " rejected due to power source") + 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 + engine.Autolevel:autoLevel(b) + until last_stats == b.unused_stats or b.unused_stats <= 0 + end end --- Creates a random Boss (or elite) actor @@ -2143,7 +2202,7 @@ function _M:createRandomBoss(base, data) b.name = name.." the "..b.name end print("Creating random boss ", b.name, data.level, "level", data.nb_classes, "classes") - if data.force_classes then print(" * forcing classes:",table.concat(table.keys(data.force_classes),",")) end + if data.force_classes then print(" * force_classes:", (string.fromTable(data.force_classes))) end b.unique = b.name b.randboss = true local boss_id = "RND_BOSS_"..b.name:upper():gsub("[^A-Z]", "_") @@ -2174,7 +2233,8 @@ function _M:createRandomBoss(base, data) b.inven = {} 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 } b:initBody() - + -- don't auto equip inventory if forbidden + if data.forbid_equip then b.inven[b.INVEN_INVEN]._no_equip_objects = true end b:resolve() -- Start with sustains sustained b[#b+1] = resolvers.sustains_at_birth() @@ -2183,11 +2243,21 @@ function _M:createRandomBoss(base, data) b.autolevel = "random_boss" b.auto_stats = {} - -- Remove default equipment, if any - local todel = {} - for k, resolver in pairs(b) do if type(resolver) == "table" and resolver.__resolver and (resolver.__resolver == "equip" or resolver.__resolver == "drops") then todel[#todel+1] = k end end - for _, k in ipairs(todel) do b[k] = nil end - + -- Update default equipment, if any, to "boss" levels + for k, resolver in ipairs(b) do + if type(resolver) == "table" and resolver.__resolver == "equip" then + resolver[1].id = nil + for i, d in ipairs(resolver[1]) do + d.name, d.id = nil, nil + d.ego_chance = nil + d.ignore_material_restriction = true + d.forbid_power_source = b.not_power_source + d.tome_drops = data.loot_quality or "boss" + d.force_drop = (data.drop_equipment == nil) and true or data.drop_equipment + end + end + end + -- Boss worthy drops b[#b+1] = resolvers.drops{chance=100, nb=data.loot_quantity or 3, {tome_drops=data.loot_quality or "boss"} } if not data.no_loot_randart then b[#b+1] = resolvers.drop_randart{} end @@ -2212,14 +2282,8 @@ function _M:createRandomBoss(base, data) b._rndboss_talent_cds = data.talent_cds_factor b.on_added_to_level = function(self, ...) self:check("birth_create_alchemist_golem") - for tid, lev in pairs(self.learn_tids) do - if self:getTalentLevelRaw(tid) < lev then - self:learnTalent(tid, true, lev - self:getTalentLevelRaw(tid)) - end - end self:check("rnd_boss_on_added_to_level", ...) self.rnd_boss_on_added_to_level = nil - self.learn_tids = nil self.on_added_to_level = nil -- Increase talent cds diff --git a/game/modules/tome/class/NPC.lua b/game/modules/tome/class/NPC.lua index e64214c51c15d87a9198a7127f3838f3c50cac7d..97e37b0595c91a220025c7e878bf642faad5948b 100644 --- a/game/modules/tome/class/NPC.lua +++ b/game/modules/tome/class/NPC.lua @@ -77,7 +77,7 @@ function _M:act() if self.emote_random and self.x and self.y and game.level.map.seens(self.x, self.y) and rng.range(0, 999) < self.emote_random.chance * 10 then local e = util.getval(rng.table(self.emote_random)) if e then - local dur = util.bound(#e, 30, 90) + local dur = util.bound(#e, 45, 90) self:doEmote(e, dur) end end @@ -474,25 +474,22 @@ function _M:addedToLevel(level, x, y) for tid, lev in pairs(self.talents) do self:learnTalent(tid, true, math.floor(lev / 2)) end - -- Give unrand bosses extra classes + -- Give randbosses extra classes if not self.randboss and self.rank >= 3.5 and not self.no_difficulty_random_class then - local data = {} - if self.rank == 3.5 then data = {nb_classes=1} - elseif self.rank == 4 then data = {nb_classes=1} - elseif self.rank == 5 then data = {nb_classes=2} - elseif self.rank >= 10 then data = {nb_classes=3} + local nb_classes = 0 + if self.rank >= 10 then nb_classes = 3 + elseif self.rank >= 5 then nb_classes = 2 + elseif self.rank >= 3.5 then nb_classes = 1 end - data.auto_sustain = true - data.forbid_equip = true + 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"} game.state:applyRandomClass(self, data, true) + self:resolve() self:resolve(nil, true) end -- Increase life self.max_life_no_difficulty_boost = self.max_life local lifeadd = self.max_life * 0.2 self.max_life = self.max_life + lifeadd self.life = self.life + lifeadd - -- print("Insane increasing " .. self.name .. " life by " .. lifeadd) - self:attr("difficulty_boosted", 1) elseif game.difficulty == game.DIFFICULTY_MADNESS then -- Increase talent level @@ -500,15 +497,15 @@ function _M:addedToLevel(level, x, y) self:learnTalent(tid, true, math.ceil(lev * 1.7)) end if not self.randboss and self.rank >= 3.5 and not self.no_difficulty_random_class then - local data = {} - if self.rank == 3.5 then data = {nb_classes=1} - elseif self.rank == 4 then data = {nb_classes=2} - elseif self.rank == 5 then data = {nb_classes=3} - elseif self.rank >= 10 then data = {nb_classes=5} + local nb_classes = 0 + if self.rank >= 10 then nb_classes = 5 + elseif self.rank >= 5 then nb_classes = 3 + elseif self.rank >= 4 then nb_classes = 2 + elseif self.rank >= 3.5 then nb_classes = 1 end - data.auto_sustain = true - data.forbid_equip = true + 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"} game.state:applyRandomClass(self, data, true) + self:resolve() self:resolve(nil, true) end -- Increase life self.max_life_no_difficulty_boost = self.max_life @@ -518,37 +515,8 @@ function _M:addedToLevel(level, x, y) self:attr("difficulty_boosted", 1) end - - -- try to equip inventory items - local MainInven, o = self:getInven(self.INVEN_INVEN) - ---if config.settings.cheat then self:inventoryApplyAll(function(inv, item, o) o:identify(true) end) end-- temp - - if MainInven then --try to equip items from inventory - for i = #MainInven, 1, -1 do - o = MainInven[i] - local inven, worn = self:getInven(o:wornInven()) - if inven and game.state:checkPowers(self, o, nil, "antimagic_only") then -- check antimagic restrictions - local ro, replace = inven and inven[1], false - o = self:removeObject(self.INVEN_INVEN, i) - if o then - - -- could put more sophisticated criteria here to pick the best gear - if ro and o.type == ro.type and o.subtype == ro.subtype and (o.rare or o.randart or o.unique) and not (ro.rare or ro.randart or ro.unique) then replace = true end - worn = self:wearObject(o, replace, false) - if worn then - print("[NPC:addedToLevel]", self.name, self.uid, "wearing", o.name) - if type(worn) == "table" then - print("--- replacing", worn.name) - self:addObject(self.INVEN_INVEN, worn) - end - else - self:addObject(self.INVEN_INVEN, o) -- put object back in main inventory - end - end - end - end - end + + self:wearAllInventory() if self:knowTalent(self.T_COMMAND_STAFF) then -- make sure staff aspect is appropriate to talents self:forceUseTalent(self.T_COMMAND_STAFF, {ignore_energy = true, ignore_cd=true, silent=true}) end diff --git a/game/modules/tome/class/Object.lua b/game/modules/tome/class/Object.lua index 83baec62ddcc779e7ab34277bfbca6ae9a0b263c..9742de4c5bf08c04b8b050ad068eddfd962be465 100644 --- a/game/modules/tome/class/Object.lua +++ b/game/modules/tome/class/Object.lua @@ -46,57 +46,12 @@ _M._special_ego_rules = {special_on_hit=true, special_on_crit=true, special_on_k function _M:getRequirementDesc(who) local base_getRequirementDesc = engine.Object.getRequirementDesc - if self.subtype == "shield" and type(self.require) == "table" and who:knowTalent(who.T_SKIRMISHER_BUCKLER_EXPERTISE) then - local oldreq = rawget(self, "require") - self.require = table.clone(oldreq, true) - if self.require.stat and self.require.stat.str then - self.require.stat.cun, self.require.stat.str = self.require.stat.str, nil - end - if self.require.talent then for i, tr in ipairs(self.require.talent) do - if tr[1] == who.T_ARMOUR_TRAINING then - self.require.talent[i] = {who.T_SKIRMISHER_BUCKLER_EXPERTISE, 1} - break - end - end end - - local desc = base_getRequirementDesc(self, who) - - self.require = oldreq - - return desc - elseif self.subtype == "shield" and type(self.require) == "table" and who:knowTalent(who.T_AGILE_DEFENSE) then - local oldreq = rawget(self, "require") - self.require = table.clone(oldreq, true) - if self.require.stat and self.require.stat.str then - self.require.stat.dex, self.require.stat.str = self.require.stat.str, nil - end - if self.require.talent then for i, tr in ipairs(self.require.talent) do - if tr[1] == who.T_ARMOUR_TRAINING then - self.require.talent[i] = {who.T_AGILE_DEFENSE, 1} - break - end - end end - - local desc = base_getRequirementDesc(self, who) - - self.require = oldreq - - return desc - elseif (self.type =="weapon" or self.type=="ammo") and type(self.require) == "table" and who:knowTalent(who.T_STRENGTH_OF_PURPOSE) then - local oldreq = rawget(self, "require") - self.require = table.clone(oldreq, true) - if self.require.stat and self.require.stat.str then - self.require.stat.mag, self.require.stat.str = self.require.stat.str, nil - end - - local desc = base_getRequirementDesc(self, who) - - self.require = oldreq - - return desc - else - return base_getRequirementDesc(self, who) - end + + local oldreq + self.require, oldreq = who:updateObjectRequirements(self) + local ret = base_getRequirementDesc(self, who) + self.require = oldreq + return ret end local auto_moddable_tile_slots = { @@ -358,6 +313,70 @@ function _M:use(who, typ, inven, item) end end +--- Find the best locations (inventory and slot) to try to wear an object in +-- applies inventory filters, optionally sorted, does not check if the object can actually be worn +-- @param use_actor: the actor to wear the object +-- @param weight_fn[1]: a function(o, inven) returning a weight value for an object +-- default is (1 + o:getPowerRank())*o.material_level, (0 for no object) +-- @param weight_fn[2]: true weight is 1 (object) or 0 (no object) return empty locations (sorted) +-- @param weight_fn[3]: false weight is 1 (object) or 0 (no object) return all locations (unsorted) +-- @param filter_field: field to check in each inventory for an object filter (defaults: "auto_equip_filter") +-- (sets filter._equipping_entity == use_actor before testing the filter) +-- @param no_type_check: set to allow locations with objects of different type/subtype (automatic if a filter is defined) +-- @return[1] nil if no locations could be found +-- @return[2] an ordered list (table) of locations where the object can be worn, each with format: +-- {inv=inventory (table), wt=sort weight, slot=slot within inventory} +-- The sort weight for each location is computed = weight_fn(self, inven)-weight_fn(worn object, inven) +-- (weight for objects that fail inventory filter checks is 0) +-- The list is sorted by descending weight, removing locations with sort weight <= 0 +function _M:wornLocations(use_actor, weight_fn, filter_field, no_type_check) + if not use_actor then return end + filter_field = filter_field == nil and "auto_equip_filter" or filter_field + if weight_fn == nil then + weight_fn = function(o, inven) return (1 + o:getPowerRank())*(o.material_level or 1) end + elseif weight_fn == true then + weight_fn = function(o, inven) return o and 1 or 0 end + end + -- considers main and offslot (could check others here) + -- Note: psionic focus needs code similar to that in the Telekinetic Grasp talent + local inv_ids = {self:wornInven()} + inv_ids[#inv_ids+1] = use_actor:getObjectOffslot(self) + local invens = {} + local new_wt = weight_fn and weight_fn(self) or 1 + --print("[Object:wornLocations] found inventories", self.uid, self.name) table.print(inv_ids) + for i, id in ipairs(inv_ids) do + local inv = use_actor:getInven(id) + if inv then + local flt = inv[filter_field] + local match_types = not (no_type_check or flt) + if flt then + flt._equipping_entity = use_actor + if not game.zone:checkFilter(self, flt, "object") then inv = nil end + end + if inv then + local inv_name = use_actor:getInvenDef(id).short_name + for k = 1, math.min(inv.max, #inv + 1) do + local wo, wt = inv[k], new_wt + if wo then + if match_types and (self.type ~= wo.type or self.subtype ~= wo.subtype) and (inv_name == wo.slot or inv_name == use_actor:getObjectOffslot(wo)) then + wt = 0 + elseif not flt or game.zone:checkFilter(wo, flt, "object") then + wt = wt - (weight_fn and weight_fn(wo) or 1) + end + end + if weight_fn == false or wt > 0 then invens[#invens+1] = {inv=inv, wt=wt, slot=k} end + if not wo then break end -- 1st open inventory slot + end + end + if flt then flt._equipping_entity = nil end + end + end + if #invens > 0 then + if weight_fn then table.sort(invens, function(a, b) return a.wt > b.wt end) end + return invens + end +end + --- Returns a tooltip for the object function _M:tooltip(x, y, use_actor) local str = self:getDesc({do_color=true}, game.player:getInven(self:wornInven())) @@ -465,8 +484,9 @@ function _M:getPowerRank() if self.godslayer then return 10 end if self.legendary then return 5 end if self.unique then return 3 end - if self.egoed and self.greater_ego then return 2 end - if self.egoed or self.rare then return 1 end + if self.egoed then + return math.min(2.5, 1 + (self.greater_ego and self.greater_ego or 0) + (self.rare and 1 or 0)) + end return 0 end @@ -615,6 +635,515 @@ function _M:descAccuracyBonus(desc, weapon, use_actor) end end +--- Static +function _M:compareFields(item1, items, infield, field, outformat, text, mod, isinversed, isdiffinversed, add_table) + add_table = add_table or {} + mod = mod or 1 + isinversed = isinversed or false + isdiffinversed = isdiffinversed or false + local ret = tstring{} + local added = 0 + local add = false + ret:add(text) + local outformatres + local resvalue = ((item1[field] or 0) + (add_table[field] or 0)) * mod + local item1value = resvalue + if type(outformat) == "function" then + outformatres = outformat(resvalue, nil) + else outformatres = outformat:format(resvalue) end + if isinversed then + ret:add(((item1[field] or 0) + (add_table[field] or 0)) > 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) + else + ret:add(((item1[field] or 0) + (add_table[field] or 0)) < 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) + end + if item1[field] then + add = true + end + for i=1, #items do + if items[i][infield] and items[i][infield][field] then + if added == 0 then + ret:add(" (") + elseif added > 1 then + ret:add(" / ") + end + added = added + 1 + add = true + if items[i][infield][field] ~= (item1[field] or 0) then + local outformatres + local resvalue = (items[i][infield][field] + (add_table[field] or 0)) * mod + if type(outformat) == "function" then + outformatres = outformat(item1value, resvalue) + else outformatres = outformat:format(item1value - resvalue) end + if isdiffinversed then + ret:add(items[i][infield][field] < (item1[field] or 0) and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) + else + ret:add(items[i][infield][field] > (item1[field] or 0) and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) + end + else + ret:add("-") + end + end + end + if added > 0 then + ret:add(")") + end + if add then + ret:add(true) + return ret + end +end + +function _M:compareTableFields(item1, items, infield, field, outformat, text, kfunct, mod, isinversed, filter) + mod = mod or 1 + isinversed = isinversed or false + local ret = tstring{} + local added = 0 + local add = false + ret:add(text) + local tab = {} + if item1[field] then + for k, v in pairs(item1[field]) do + tab[k] = {} + tab[k][1] = v + end + end + for i=1, #items do + if items[i][infield] and items[i][infield][field] then + for k, v in pairs(items[i][infield][field]) do + tab[k] = tab[k] or {} + tab[k][i + 1] = v + end + end + end + local count1 = 0 + for k, v in pairs(tab) do + if not filter or filter(k, v) then + local count = 0 + if isinversed then + ret:add(("%s"):format((count1 > 0) and " / " or ""), (v[1] or 0) > 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0)), {"color","LAST"}) + else + ret:add(("%s"):format((count1 > 0) and " / " or ""), (v[1] or 0) < 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0)), {"color","LAST"}) + end + count1 = count1 + 1 + if v[1] then + add = true + end + for kk, vv in pairs(v) do + if kk > 1 then + if count == 0 then + ret:add("(") + elseif count > 0 then + ret:add(" / ") + end + if vv ~= (v[1] or 0) then + if isinversed then + ret:add((v[1] or 0) > vv and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0) - vv), {"color","LAST"}) + else + ret:add((v[1] or 0) < vv and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0) - vv), {"color","LAST"}) + end + else + ret:add("-") + end + add = true + count = count + 1 + end + end + if count > 0 then + ret:add(")") + end + ret:add(kfunct(k)) + end + end + + if add then + ret:add(true) + return ret + end +end + +--- Static +function _M:descCombat(use_actor, combat, compare_with, field, add_table, is_fake_add) + local desc = tstring{} + add_table = add_table or {} + add_table.dammod = add_table.dammod or {} + combat = table.clone(combat[field] or {}) + compare_with = compare_with or {} + + local compare_fields = function(item1, items, infield, field, outformat, text, mod, isinversed, isdiffinversed, add_table) + local add = self:compareFields(item1, items, infield, field, outformat, text, mod, isinversed, isdiffinversed, add_table) + if add then desc:merge(add) end + end + local compare_table_fields = function(item1, items, infield, field, outformat, text, kfunct, mod, isinversed, filter) + local add = self:compareTableFields(item1, items, infield, field, outformat, text, kfunct, mod, isinversed, filter) + if add then desc:merge(add) end + end + + local dm = {} + combat.dammod = table.mergeAdd(table.clone(combat.dammod or {}), add_table.dammod) + local dammod = use_actor:getDammod(combat) + for stat, i in pairs(dammod) do + local name = Stats.stats_def[stat].short_name:capitalize() + if use_actor:knowTalent(use_actor.T_STRENGTH_OF_PURPOSE) then + if name == "Str" then name = "Mag" end + end + if self.subtype == "dagger" and use_actor:knowTalent(use_actor.T_LETHALITY) then + if name == "Str" then name = "Cun" end + end + dm[#dm+1] = ("%d%% %s"):format(i * 100, name) + end + if #dm > 0 or combat.dam then + local diff_count = 0 + local any_diff = false + if config.settings.tome.advanced_weapon_stats then + local base_power = use_actor:combatDamagePower(combat, add_table.dam) + local base_range = use_actor:combatDamageRange(combat, add_table.damrange) + local power_diff, range_diff = {}, {} + for _, v in ipairs(compare_with) do + if v[field] then + local base_power_diff = base_power - use_actor:combatDamagePower(v[field], add_table.dam) + local base_range_diff = base_range - use_actor:combatDamageRange(v[field], add_table.damrange) + power_diff[#power_diff + 1] = ("%s%+d%%#LAST#"):format(base_power_diff > 0 and "#00ff00#" or "#ff0000#", base_power_diff * 100) + range_diff[#range_diff + 1] = ("%s%+.1fx#LAST#"):format(base_range_diff > 0 and "#00ff00#" or "#ff0000#", base_range_diff) + diff_count = diff_count + 1 + if base_power_diff ~= 0 or base_range_diff ~= 0 then + any_diff = true + end + end + end + if any_diff then + local s = ("Power: %3d%% (%s) Range: %.1fx (%s)"):format(base_power * 100, table.concat(power_diff, " / "), base_range, table.concat(range_diff, " / ")) + desc:merge(s:toTString()) + else + desc:add(("Power: %3d%% Range: %.1fx"):format(base_power * 100, base_range)) + end + else + local power_diff = {} + for i, v in ipairs(compare_with) do + if v[field] then + local base_power_diff = ((combat.dam or 0) + (add_table.dam or 0)) - ((v[field].dam or 0) + (add_table.dam or 0)) + local dfl_range = (1.1 - (add_table.damrange or 0)) + local multi_diff = (((combat.damrange or dfl_range) + (add_table.damrange or 0)) * ((combat.dam or 0) + (add_table.dam or 0))) - (((v[field].damrange or dfl_range) + (add_table.damrange or 0)) * ((v[field].dam or 0) + (add_table.dam or 0))) + power_diff [#power_diff + 1] = ("%s%+.1f#LAST# - %s%+.1f#LAST#"):format(base_power_diff > 0 and "#00ff00#" or "#ff0000#", base_power_diff, multi_diff > 0 and "#00ff00#" or "#ff0000#", multi_diff) + diff_count = diff_count + 1 + if base_power_diff ~= 0 or multi_diff ~= 0 then + any_diff = true + end + end + end + if any_diff == false then + power_diff = "" + else + power_diff = ("(%s)"):format(table.concat(power_diff, " / ")) + end + desc:add(("Base power: %.1f - %.1f"):format((combat.dam or 0) + (add_table.dam or 0), ((combat.damrange or (1.1 - (add_table.damrange or 0))) + (add_table.damrange or 0)) * ((combat.dam or 0) + (add_table.dam or 0)))) + desc:merge(power_diff:toTString()) + end + desc:add(true) + desc:add(("Uses stat%s: %s"):format(#dm > 1 and "s" or "",table.concat(dm, ', ')), true) + local col = (combat.damtype and DamageType:get(combat.damtype) and DamageType:get(combat.damtype).text_color or "#WHITE#"):toTString() + desc:add("Damage type: ", col[2],DamageType:get(combat.damtype or DamageType.PHYSICAL).name:capitalize(),{"color","LAST"}, true) + end + + if combat.talented then + local t = use_actor:combatGetTraining(combat) + if t and t.name then desc:add("Mastery: ", {"color","GOLD"}, t.name, {"color","LAST"}, true) end + end + + self:descAccuracyBonus(desc, combat, use_actor) + + if combat.wil_attack then + desc:add("Accuracy is based on willpower for this weapon.", true) + end + + compare_fields(combat, compare_with, field, "atk", "%+d", "Accuracy: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "apr", "%+d", "Armour Penetration: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "physcrit", "%+.1f%%", "Crit. chance: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "crit_power", "%+.1f%%", "Crit. power: ", 1, false, false, add_table) + local physspeed_compare = function(orig, compare_with) + orig = 100 / orig + if compare_with then return ("%+.0f%%"):format(orig - 100 / compare_with) + else return ("%2.0f%%"):format(orig) end + end + compare_fields(combat, compare_with, field, "physspeed", physspeed_compare, "Attack speed: ", 1, false, true, add_table) + + compare_fields(combat, compare_with, field, "block", "%+d", "Block value: ", 1, false, false, add_table) + + compare_fields(combat, compare_with, field, "dam_mult", "%d%%", "Dam. multiplier: ", 100, false, false, add_table) + compare_fields(combat, compare_with, field, "range", "%+d", "Firing range: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "capacity", "%d", "Capacity: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "shots_reloaded_per_turn", "%+d", "Reload speed: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "ammo_every", "%d", "Turns elapse between self-loadings: ", 1, false, false, add_table) + + local talents = {} + if combat.talent_on_hit then + for tid, data in pairs(combat.talent_on_hit) do + talents[tid] = {data.chance, data.level} + end + end + for i, v in ipairs(compare_with or {}) do + for tid, data in pairs(v[field] and (v[field].talent_on_hit or {})or {}) do + if not talents[tid] or talents[tid][1]~=data.chance or talents[tid][2]~=data.level then + desc:add({"color","RED"}, ("When this weapon hits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, data.chance, data.level), {"color","LAST"}, true) + else + talents[tid][3] = true + end + end + end + for tid, data in pairs(talents) do + desc:add(talents[tid][3] and {"color","WHITE"} or {"color","GREEN"}, ("When this weapon hits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, talents[tid][1], talents[tid][2]), {"color","LAST"}, true) + end + + local talents = {} + if combat.talent_on_crit then + for tid, data in pairs(combat.talent_on_crit) do + talents[tid] = {data.chance, data.level} + end + end + for i, v in ipairs(compare_with or {}) do + for tid, data in pairs(v[field] and (v[field].talent_on_crit or {})or {}) do + if not talents[tid] or talents[tid][1]~=data.chance or talents[tid][2]~=data.level then + desc:add({"color","RED"}, ("When this weapon crits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, data.chance, data.level), {"color","LAST"}, true) + else + talents[tid][3] = true + end + end + end + for tid, data in pairs(talents) do + desc:add(talents[tid][3] and {"color","WHITE"} or {"color","GREEN"}, ("When this weapon crits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, talents[tid][1], talents[tid][2]), {"color","LAST"}, true) + end + + local special = "" + if combat.special_on_hit then + special = combat.special_on_hit.desc + end + + --[[ I couldn't figure out how to make this work because tdesc goes in the same list as special_on_Hit + local found = false + for i, v in ipairs(compare_with or {}) do + if v[field] and v[field].special_on_hit then + if special ~= v[field].special_on_hit.desc then + desc:add({"color","RED"}, "When this weapon hits: "..v[field].special_on_hit.desc, {"color","LAST"}, true) + else + found = true + end + end + end + --]] + + -- get_items takes the combat table and returns a table of items to print. + -- Each of these items one of the following: + -- id -> {priority, string} + -- id -> {priority, message_function(this, compared), value} + -- header is the section header. + local compare_list = function(header, get_items) + local priority_ordering = function(left, right) + return left[2][1] < right[2][1] + end + + if next(compare_with) then + -- Grab the left and right items. + local left = get_items(combat) + local right = {} + for i, v in ipairs(compare_with) do + for k, item in pairs(get_items(v[field])) do + if not right[k] then + right[k] = item + elseif type(right[k]) == 'number' then + right[k] = right[k] + item + else + right[k] = item + end + end + end + + -- Exit early if no items. + if not next(left) and not next(right) then return end + + desc:add(header, true) + + local combined = table.clone(left) + table.merge(combined, right) + + for k, _ in table.orderedPairs2(combined, priority_ordering) do + l = left[k] + r = right[k] + message = (l and l[2]) or (r and r[2]) + if type(message) == 'function' then + desc:add(message(l and l[3], r and r[3] or 0), true) + elseif type(message) == 'string' then + local prefix = '* ' + local color = 'WHITE' + if l and not r then + color = 'GREEN' + prefix = '+ ' + end + if not l and r then + color = 'RED' + prefix = '- ' + end + desc:add({'color',color}, prefix, message, {'color','LAST'}, true) + end + end + else + local items = get_items(combat) + if next(items) then + desc:add(header, true) + for k, v in table.orderedPairs2(items, priority_ordering) do + message = v[2] + if type(message) == 'function' then + desc:add(message(v[3]), true) + elseif type(message) == 'string' then + desc:add({'color','WHITE'}, '* ', message, {'color','LAST'}, true) + end + end + end + end + end + + local get_special_list = function(combat, key) + local special = combat[key] + + -- No special + if not special then return {} end + -- Single special + if special.desc then + return {[special.desc] = {10, util.getval(special.desc, self, use_actor, special)}} + end + + -- Multiple specials + local list = {} + for _, special in pairs(special) do + list[special.desc] = {10, util.getval(special.desc, self, use_actor, special)} + end + return list + end + + compare_list( + "On weapon hit:", + function(combat) + if not combat then return {} end + local list = {} + -- Get complex damage types + for dt, amount in pairs(combat.melee_project or combat.ranged_project or {}) do + local dt_def = DamageType:get(dt) + if dt_def and dt_def.tdesc then + list[dt] = {0, dt_def.tdesc, amount} + end + end + -- Get specials + table.merge(list, get_special_list(combat, 'special_on_hit')) + return list + end + ) + + compare_list( + "On weapon crit:", + function(combat) + if not combat then return {} end + return get_special_list(combat, 'special_on_crit') + end + ) + + compare_list( + "On weapon kill:", + function(combat) + if not combat then return {} end + return get_special_list(combat, 'special_on_kill') + end + ) + + local found = false + for i, v in ipairs(compare_with or {}) do + if v[field] and v[field].no_stealth_break then + found = true + end + end + + if combat.no_stealth_break then + desc:add(found and {"color","WHITE"} or {"color","GREEN"},"When used from stealth a simple attack with it will not break stealth.", {"color","LAST"}, true) + elseif found then + desc:add({"color","RED"}, "When used from stealth a simple attack with it will not break stealth.", {"color","LAST"}, true) + end + + if combat.crushing_blow then + desc:add({"color", "YELLOW"}, "Crushing Blows: ", {"color", "LAST"}, "Damage dealt by this weapon is increased by half your critical multiplier, if doing so would kill the target.", true) + end + + compare_fields(combat, compare_with, field, "travel_speed", "%+d%%", "Travel speed: ", 100, false, false, add_table) + + compare_fields(combat, compare_with, field, "phasing", "%+d%%", "Damage Shield penetration (this weapon only): ", 1, false, false, add_table) + + compare_fields(combat, compare_with, field, "lifesteal", "%+d%%", "Lifesteal (this weapon only): ", 1, false, false, add_table) + + local attack_recurse_procs_reduce_compare = function(orig, compare_with) + orig = 100 - 100 / orig + if compare_with then return ("%+d%%"):format(-(orig - (100 - 100 / compare_with))) + else return ("%d%%"):format(-orig) end + end + compare_fields(combat, compare_with, field, "attack_recurse", "%+d", "Multiple attacks: ", 1, false, false, add_table) + compare_fields(combat, compare_with, field, "attack_recurse_procs_reduce", attack_recurse_procs_reduce_compare, "Multiple attacks procs power reduction: ", 1, true, false, add_table) + + if combat.tg_type and combat.tg_type == "beam" then + desc:add({"color","YELLOW"}, ("Shots beam through all targets."), {"color","LAST"}, true) + end + + compare_table_fields( + combat, compare_with, field, "melee_project", "%+d", "Damage (Melee): ", + function(item) + local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() + return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} + end, + nil, nil, + function(k, v) return not DamageType.dam_def[k].tdesc end) + + compare_table_fields( + combat, compare_with, field, "ranged_project", "%+d", "Damage (Ranged): ", + function(item) + local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() + return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} + end, + nil, nil, + function(k, v) return not DamageType.dam_def[k].tdesc end) + + compare_table_fields(combat, compare_with, field, "burst_on_hit", "%+d", "Burst (radius 1) on hit: ", function(item) + local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() + return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} + end) + + compare_table_fields(combat, compare_with, field, "burst_on_crit", "%+d", "Burst (radius 2) on crit: ", function(item) + local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() + return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} + end) + + compare_table_fields(combat, compare_with, field, "convert_damage", "%d%%", "Damage conversion: ", function(item) + local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() + return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} + end) + + compare_table_fields(combat, compare_with, field, "inc_damage_type", "%+d%% ", "Damage against: ", function(item) + local _, _, t, st = item:find("^([^/]+)/?(.*)$") + if st and st ~= "" then + return st:capitalize() + else + return t:capitalize() + end + end) + + -- resources used to attack + compare_table_fields( + combat, compare_with, field, "use_resources", "%0.1f", "#ORANGE#Attacks use: #LAST#", + function(item) + local res_def = ActorResource.resources_def[item] + local col = (res_def and res_def.color or "#SALMON#"):toTString() + return col[2], (" %s"):format(res_def and res_def.name or item:capitalize()),{"color","LAST"} + end, + nil, + true) + + self:triggerHook{"Object:descCombat", compare_with=compare_with, compare_fields=compare_fields, compare_scaled=compare_scaled, compare_scaled=compare_scaled, compare_table_fields=compare_table_fields, desc=desc, combat=combat} + return desc +end + --- Gets the full textual desc of the object without the name and requirements function _M:getTextualDesc(compare_with, use_actor) use_actor = use_actor or game.player @@ -705,60 +1234,8 @@ function _M:getTextualDesc(compare_with, use_actor) end local compare_fields = function(item1, items, infield, field, outformat, text, mod, isinversed, isdiffinversed, add_table) - add_table = add_table or {} - mod = mod or 1 - isinversed = isinversed or false - isdiffinversed = isdiffinversed or false - local ret = tstring{} - local added = 0 - local add = false - ret:add(text) - local outformatres - local resvalue = ((item1[field] or 0) + (add_table[field] or 0)) * mod - local item1value = resvalue - if type(outformat) == "function" then - outformatres = outformat(resvalue, nil) - else outformatres = outformat:format(resvalue) end - if isinversed then - ret:add(((item1[field] or 0) + (add_table[field] or 0)) > 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) - else - ret:add(((item1[field] or 0) + (add_table[field] or 0)) < 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) - end - if item1[field] then - add = true - end - for i=1, #items do - if items[i][infield] and items[i][infield][field] then - if added == 0 then - ret:add(" (") - elseif added > 1 then - ret:add(" / ") - end - added = added + 1 - add = true - if items[i][infield][field] ~= (item1[field] or 0) then - local outformatres - local resvalue = (items[i][infield][field] + (add_table[field] or 0)) * mod - if type(outformat) == "function" then - outformatres = outformat(item1value, resvalue) - else outformatres = outformat:format(item1value - resvalue) end - if isdiffinversed then - ret:add(items[i][infield][field] < (item1[field] or 0) and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) - else - ret:add(items[i][infield][field] > (item1[field] or 0) and {"color","RED"} or {"color","LIGHT_GREEN"}, outformatres, {"color", "LAST"}) - end - else - ret:add("-") - end - end - end - if added > 0 then - ret:add(")") - end - if add then - desc:merge(ret) - desc:add(true) - end + local add = self:compareFields(item1, items, infield, field, outformat, text, mod, isinversed, isdiffinversed, add_table) + if add then desc:merge(add) end end -- included - if we should include the value in the present total. @@ -778,441 +1255,13 @@ function _M:getTextualDesc(compare_with, use_actor) end local compare_table_fields = function(item1, items, infield, field, outformat, text, kfunct, mod, isinversed, filter) - mod = mod or 1 - isinversed = isinversed or false - local ret = tstring{} - local added = 0 - local add = false - ret:add(text) - local tab = {} - if item1[field] then - for k, v in pairs(item1[field]) do - tab[k] = {} - tab[k][1] = v - end - end - for i=1, #items do - if items[i][infield] and items[i][infield][field] then - for k, v in pairs(items[i][infield][field]) do - tab[k] = tab[k] or {} - tab[k][i + 1] = v - end - end - end - local count1 = 0 - for k, v in pairs(tab) do - if not filter or filter(k, v) then - local count = 0 - if isinversed then - ret:add(("%s"):format((count1 > 0) and " / " or ""), (v[1] or 0) > 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0)), {"color","LAST"}) - else - ret:add(("%s"):format((count1 > 0) and " / " or ""), (v[1] or 0) < 0 and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0)), {"color","LAST"}) - end - count1 = count1 + 1 - if v[1] then - add = true - end - for kk, vv in pairs(v) do - if kk > 1 then - if count == 0 then - ret:add("(") - elseif count > 0 then - ret:add(" / ") - end - if vv ~= (v[1] or 0) then - if isinversed then - ret:add((v[1] or 0) > vv and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0) - vv), {"color","LAST"}) - else - ret:add((v[1] or 0) < vv and {"color","RED"} or {"color","LIGHT_GREEN"}, outformat:format((v[1] or 0) - vv), {"color","LAST"}) - end - else - ret:add("-") - end - add = true - count = count + 1 - end - end - if count > 0 then - ret:add(")") - end - ret:add(kfunct(k)) - end - end - - if add then - desc:merge(ret) - desc:add(true) - end + local add = self:compareTableFields(item1, items, infield, field, outformat, text, kfunct, mod, isinversed, filter) + if add then desc:merge(add) end end - local desc_combat = function(combat, compare_with, field, add_table, is_fake_add) - add_table = add_table or {} - add_table.dammod = add_table.dammod or {} - combat = table.clone(combat[field] or {}) - compare_with = compare_with or {} - local dm = {} - combat.dammod = table.mergeAdd(table.clone(combat.dammod or {}), add_table.dammod) - local dammod = use_actor:getDammod(combat) - for stat, i in pairs(dammod) do - local name = Stats.stats_def[stat].short_name:capitalize() - if use_actor:knowTalent(use_actor.T_STRENGTH_OF_PURPOSE) then - if name == "Str" then name = "Mag" end - end - if self.subtype == "dagger" and use_actor:knowTalent(use_actor.T_LETHALITY) then - if name == "Str" then name = "Cun" end - end - dm[#dm+1] = ("%d%% %s"):format(i * 100, name) - end - if #dm > 0 or combat.dam then - local diff_count = 0 - local any_diff = false - if config.settings.tome.advanced_weapon_stats then - local base_power = use_actor:combatDamagePower(combat, add_table.dam) - local base_range = use_actor:combatDamageRange(combat, add_table.damrange) - local power_diff, range_diff = {}, {} - for _, v in ipairs(compare_with) do - if v[field] then - local base_power_diff = base_power - use_actor:combatDamagePower(v[field], add_table.dam) - local base_range_diff = base_range - use_actor:combatDamageRange(v[field], add_table.damrange) - power_diff[#power_diff + 1] = ("%s%+d%%#LAST#"):format(base_power_diff > 0 and "#00ff00#" or "#ff0000#", base_power_diff * 100) - range_diff[#range_diff + 1] = ("%s%+.1fx#LAST#"):format(base_range_diff > 0 and "#00ff00#" or "#ff0000#", base_range_diff) - diff_count = diff_count + 1 - if base_power_diff ~= 0 or base_range_diff ~= 0 then - any_diff = true - end - end - end - if any_diff then - local s = ("Power: %3d%% (%s) Range: %.1fx (%s)"):format(base_power * 100, table.concat(power_diff, " / "), base_range, table.concat(range_diff, " / ")) - desc:merge(s:toTString()) - else - desc:add(("Power: %3d%% Range: %.1fx"):format(base_power * 100, base_range)) - end - else - local power_diff = {} - for i, v in ipairs(compare_with) do - if v[field] then - local base_power_diff = ((combat.dam or 0) + (add_table.dam or 0)) - ((v[field].dam or 0) + (add_table.dam or 0)) - local dfl_range = (1.1 - (add_table.damrange or 0)) - local multi_diff = (((combat.damrange or dfl_range) + (add_table.damrange or 0)) * ((combat.dam or 0) + (add_table.dam or 0))) - (((v[field].damrange or dfl_range) + (add_table.damrange or 0)) * ((v[field].dam or 0) + (add_table.dam or 0))) - power_diff [#power_diff + 1] = ("%s%+.1f#LAST# - %s%+.1f#LAST#"):format(base_power_diff > 0 and "#00ff00#" or "#ff0000#", base_power_diff, multi_diff > 0 and "#00ff00#" or "#ff0000#", multi_diff) - diff_count = diff_count + 1 - if base_power_diff ~= 0 or multi_diff ~= 0 then - any_diff = true - end - end - end - if any_diff == false then - power_diff = "" - else - power_diff = ("(%s)"):format(table.concat(power_diff, " / ")) - end - desc:add(("Base power: %.1f - %.1f"):format((combat.dam or 0) + (add_table.dam or 0), ((combat.damrange or (1.1 - (add_table.damrange or 0))) + (add_table.damrange or 0)) * ((combat.dam or 0) + (add_table.dam or 0)))) - desc:merge(power_diff:toTString()) - end - desc:add(true) - desc:add(("Uses stat%s: %s"):format(#dm > 1 and "s" or "",table.concat(dm, ', ')), true) - local col = (combat.damtype and DamageType:get(combat.damtype) and DamageType:get(combat.damtype).text_color or "#WHITE#"):toTString() - desc:add("Damage type: ", col[2],DamageType:get(combat.damtype or DamageType.PHYSICAL).name:capitalize(),{"color","LAST"}, true) - end - - if combat.talented then - local t = use_actor:combatGetTraining(combat) - if t and t.name then desc:add("Mastery: ", {"color","GOLD"}, t.name, {"color","LAST"}, true) end - end - - self:descAccuracyBonus(desc, combat, use_actor) - - if combat.wil_attack then - desc:add("Accuracy is based on willpower for this weapon.", true) - end - - compare_fields(combat, compare_with, field, "atk", "%+d", "Accuracy: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "apr", "%+d", "Armour Penetration: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "physcrit", "%+.1f%%", "Crit. chance: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "crit_power", "%+.1f%%", "Crit. power: ", 1, false, false, add_table) - local physspeed_compare = function(orig, compare_with) - orig = 100 / orig - if compare_with then return ("%+.0f%%"):format(orig - 100 / compare_with) - else return ("%2.0f%%"):format(orig) end - end - compare_fields(combat, compare_with, field, "physspeed", physspeed_compare, "Attack speed: ", 1, false, true, add_table) - - compare_fields(combat, compare_with, field, "block", "%+d", "Block value: ", 1, false, false, add_table) - - compare_fields(combat, compare_with, field, "dam_mult", "%d%%", "Dam. multiplier: ", 100, false, false, add_table) - compare_fields(combat, compare_with, field, "range", "%+d", "Firing range: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "capacity", "%d", "Capacity: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "shots_reloaded_per_turn", "%+d", "Reload speed: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "ammo_every", "%d", "Turns elapse between self-loadings: ", 1, false, false, add_table) - - local talents = {} - if combat.talent_on_hit then - for tid, data in pairs(combat.talent_on_hit) do - talents[tid] = {data.chance, data.level} - end - end - for i, v in ipairs(compare_with or {}) do - for tid, data in pairs(v[field] and (v[field].talent_on_hit or {})or {}) do - if not talents[tid] or talents[tid][1]~=data.chance or talents[tid][2]~=data.level then - desc:add({"color","RED"}, ("When this weapon hits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, data.chance, data.level), {"color","LAST"}, true) - else - talents[tid][3] = true - end - end - end - for tid, data in pairs(talents) do - desc:add(talents[tid][3] and {"color","WHITE"} or {"color","GREEN"}, ("When this weapon hits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, talents[tid][1], talents[tid][2]), {"color","LAST"}, true) - end - - local talents = {} - if combat.talent_on_crit then - for tid, data in pairs(combat.talent_on_crit) do - talents[tid] = {data.chance, data.level} - end - end - for i, v in ipairs(compare_with or {}) do - for tid, data in pairs(v[field] and (v[field].talent_on_crit or {})or {}) do - if not talents[tid] or talents[tid][1]~=data.chance or talents[tid][2]~=data.level then - desc:add({"color","RED"}, ("When this weapon crits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, data.chance, data.level), {"color","LAST"}, true) - else - talents[tid][3] = true - end - end - end - for tid, data in pairs(talents) do - desc:add(talents[tid][3] and {"color","WHITE"} or {"color","GREEN"}, ("When this weapon crits: %s (%d%% chance level %d)."):format(self:getTalentFromId(tid).name, talents[tid][1], talents[tid][2]), {"color","LAST"}, true) - end - - local special = "" - if combat.special_on_hit then - special = combat.special_on_hit.desc - end - - --[[ I couldn't figure out how to make this work because tdesc goes in the same list as special_on_Hit - local found = false - for i, v in ipairs(compare_with or {}) do - if v[field] and v[field].special_on_hit then - if special ~= v[field].special_on_hit.desc then - desc:add({"color","RED"}, "When this weapon hits: "..v[field].special_on_hit.desc, {"color","LAST"}, true) - else - found = true - end - end - end - --]] - - -- get_items takes the combat table and returns a table of items to print. - -- Each of these items one of the following: - -- id -> {priority, string} - -- id -> {priority, message_function(this, compared), value} - -- header is the section header. - local compare_list = function(header, get_items) - local priority_ordering = function(left, right) - return left[2][1] < right[2][1] - end - - if next(compare_with) then - -- Grab the left and right items. - local left = get_items(combat) - local right = {} - for i, v in ipairs(compare_with) do - for k, item in pairs(get_items(v[field])) do - if not right[k] then - right[k] = item - elseif type(right[k]) == 'number' then - right[k] = right[k] + item - else - right[k] = item - end - end - end - - -- Exit early if no items. - if not next(left) and not next(right) then return end - - desc:add(header, true) - - local combined = table.clone(left) - table.merge(combined, right) - - for k, _ in table.orderedPairs2(combined, priority_ordering) do - l = left[k] - r = right[k] - message = (l and l[2]) or (r and r[2]) - if type(message) == 'function' then - desc:add(message(l and l[3], r and r[3] or 0), true) - elseif type(message) == 'string' then - local prefix = '* ' - local color = 'WHITE' - if l and not r then - color = 'GREEN' - prefix = '+ ' - end - if not l and r then - color = 'RED' - prefix = '- ' - end - desc:add({'color',color}, prefix, message, {'color','LAST'}, true) - end - end - else - local items = get_items(combat) - if next(items) then - desc:add(header, true) - for k, v in table.orderedPairs2(items, priority_ordering) do - message = v[2] - if type(message) == 'function' then - desc:add(message(v[3]), true) - elseif type(message) == 'string' then - desc:add({'color','WHITE'}, '* ', message, {'color','LAST'}, true) - end - end - end - end - end - - local get_special_list = function(combat, key) - local special = combat[key] - - -- No special - if not special then return {} end - -- Single special - if special.desc then - return {[special.desc] = {10, util.getval(special.desc, self, use_actor, special)}} - end - - -- Multiple specials - local list = {} - for _, special in pairs(special) do - list[special.desc] = {10, util.getval(special.desc, self, use_actor, special)} - end - return list - end - - compare_list( - "On weapon hit:", - function(combat) - if not combat then return {} end - local list = {} - -- Get complex damage types - for dt, amount in pairs(combat.melee_project or combat.ranged_project or {}) do - local dt_def = DamageType:get(dt) - if dt_def and dt_def.tdesc then - list[dt] = {0, dt_def.tdesc, amount} - end - end - -- Get specials - table.merge(list, get_special_list(combat, 'special_on_hit')) - return list - end - ) - - compare_list( - "On weapon crit:", - function(combat) - if not combat then return {} end - return get_special_list(combat, 'special_on_crit') - end - ) - - compare_list( - "On weapon kill:", - function(combat) - if not combat then return {} end - return get_special_list(combat, 'special_on_kill') - end - ) - - local found = false - for i, v in ipairs(compare_with or {}) do - if v[field] and v[field].no_stealth_break then - found = true - end - end - - if combat.no_stealth_break then - desc:add(found and {"color","WHITE"} or {"color","GREEN"},"When used from stealth a simple attack with it will not break stealth.", {"color","LAST"}, true) - elseif found then - desc:add({"color","RED"}, "When used from stealth a simple attack with it will not break stealth.", {"color","LAST"}, true) - end - - if combat.crushing_blow then - desc:add({"color", "YELLOW"}, "Crushing Blows: ", {"color", "LAST"}, "Damage dealt by this weapon is increased by half your critical multiplier, if doing so would kill the target.", true) - end - - compare_fields(combat, compare_with, field, "travel_speed", "%+d%%", "Travel speed: ", 100, false, false, add_table) - - compare_fields(combat, compare_with, field, "phasing", "%+d%%", "Damage Shield penetration (this weapon only): ", 1, false, false, add_table) - - compare_fields(combat, compare_with, field, "lifesteal", "%+d%%", "Lifesteal (this weapon only): ", 1, false, false, add_table) - - local attack_recurse_procs_reduce_compare = function(orig, compare_with) - orig = 100 - 100 / orig - if compare_with then return ("%+d%%"):format(-(orig - (100 - 100 / compare_with))) - else return ("%d%%"):format(-orig) end - end - compare_fields(combat, compare_with, field, "attack_recurse", "%+d", "Multiple attacks: ", 1, false, false, add_table) - compare_fields(combat, compare_with, field, "attack_recurse_procs_reduce", attack_recurse_procs_reduce_compare, "Multiple attacks procs power reduction: ", 1, true, false, add_table) - - if combat.tg_type and combat.tg_type == "beam" then - desc:add({"color","YELLOW"}, ("Shots beam through all targets."), {"color","LAST"}, true) - end - - compare_table_fields( - combat, compare_with, field, "melee_project", "%+d", "Damage (Melee): ", - function(item) - local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() - return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} - end, - nil, nil, - function(k, v) return not DamageType.dam_def[k].tdesc end) - - compare_table_fields( - combat, compare_with, field, "ranged_project", "%+d", "Damage (Ranged): ", - function(item) - local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() - return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} - end, - nil, nil, - function(k, v) return not DamageType.dam_def[k].tdesc end) - - compare_table_fields(combat, compare_with, field, "burst_on_hit", "%+d", "Burst (radius 1) on hit: ", function(item) - local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() - return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} - end) - - compare_table_fields(combat, compare_with, field, "burst_on_crit", "%+d", "Burst (radius 2) on crit: ", function(item) - local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() - return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} - end) - - compare_table_fields(combat, compare_with, field, "convert_damage", "%d%%", "Damage conversion: ", function(item) - local col = (DamageType.dam_def[item] and DamageType.dam_def[item].text_color or "#WHITE#"):toTString() - return col[2], (" %s"):format(DamageType.dam_def[item].name),{"color","LAST"} - end) - - compare_table_fields(combat, compare_with, field, "inc_damage_type", "%+d%% ", "Damage against: ", function(item) - local _, _, t, st = item:find("^([^/]+)/?(.*)$") - if st and st ~= "" then - return st:capitalize() - else - return t:capitalize() - end - end) - - -- resources used to attack - compare_table_fields( - combat, compare_with, field, "use_resources", "%0.1f", "#ORANGE#Attacks use: #LAST#", - function(item) - local res_def = ActorResource.resources_def[item] - local col = (res_def and res_def.color or "#SALMON#"):toTString() - return col[2], (" %s"):format(res_def and res_def.name or item:capitalize()),{"color","LAST"} - end, - nil, - true) - - self:triggerHook{"Object:descCombat", compare_with=compare_with, compare_fields=compare_fields, compare_scaled=compare_scaled, compare_scaled=compare_scaled, compare_table_fields=compare_table_fields, desc=desc, combat=combat} + local desc_combat = function(...) + local cdesc = self:descCombat(use_actor, ...) + desc:merge(cdesc) end local desc_wielder = function(w, compare_with, field) diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua index 6edbea35ee812a4eee0687c816c041a039860e1e..310cc5364e3163ecee66eb71a42e47bc33724032 100644 --- a/game/modules/tome/class/Player.lua +++ b/game/modules/tome/class/Player.lua @@ -1012,7 +1012,7 @@ function _M:restCheck() -- Check for resources for res, res_def in ipairs(_M.resources_def) do if res_def.wait_on_rest and res_def.regen_prop and self:attr(res_def.regen_prop) then - if not res_def.invert_values then + if not res_def.invert_values and not res_def.switch_direction then if self[res_def.regen_prop] > 0.0001 and self:check(res_def.getFunction) < self:check(res_def.getMaxFunction) then return true end else if self[res_def.regen_prop] < -0.0001 and self:check(res_def.getFunction) > self:check(res_def.getMinFunction) then return true end diff --git a/game/modules/tome/class/UserChatExtension.lua b/game/modules/tome/class/UserChatExtension.lua index 94d93117c3656a11cf555e0d9cbed526aca9a781..0c743eab646db0cacf25c585e594b80f448ff2a9 100644 --- a/game/modules/tome/class/UserChatExtension.lua +++ b/game/modules/tome/class/UserChatExtension.lua @@ -75,6 +75,8 @@ function _M:event(e) self.chat:addMessage("link", e.channel, e.login, {e.name, color}, "#ANTIQUE_WHITE#has linked a creature: #WHITE# "..data.name, {mode="tooltip", tooltip=data.desc}) elseif data.kind == "killer-link" then self.chat:addMessage("death", e.channel, e.login, {e.name, color}, "#CRIMSON#"..data.msg.."#WHITE#", data.desc and {mode="tooltip", tooltip=data.desc} or nil) + else + self:triggerHook{"UserChat:event", color=color, e=e, data=data} end end end diff --git a/game/modules/tome/class/interface/ActorLife.lua b/game/modules/tome/class/interface/ActorLife.lua index 7025783c7c19d1efe1cf47030378ef1fa627bf17..b2b521332a58b66e48e7ae36298fa15c967e78c9 100644 --- a/game/modules/tome/class/interface/ActorLife.lua +++ b/game/modules/tome/class/interface/ActorLife.lua @@ -41,6 +41,11 @@ end function _M:takeHit(value, src, death_note) if self.onTakeHit then value = self:onTakeHit(value, src, death_note) end if value <= 0 then return false, 0 end + + if death_note and death_note.cant_die then + if value >= self.life then value = self.life - 1 end + end + self.life = self.life - value self.changed = true if self.life <= self.die_at and not self.dead then diff --git a/game/modules/tome/class/interface/Combat.lua b/game/modules/tome/class/interface/Combat.lua index 72fc581b499bad17ae41c09c5fbae941b49127c9..250791c4ce2f39807d18b3da17c4a13c8360bb42 100644 --- a/game/modules/tome/class/interface/Combat.lua +++ b/game/modules/tome/class/interface/Combat.lua @@ -274,11 +274,10 @@ end --- Computes a logarithmic chance to hit, opposing chance to hit to chance to miss -- This will be used for melee attacks, physical and spell resistance - function _M:checkHitOld(atk, def, min, max, factor) if atk < 0 then atk = 0 end if def < 0 then def = 0 end - print("checkHit", atk, def) + print("checkHitOld", atk, def) if atk == 0 then atk = 1 end local hit = nil factor = factor or 5 @@ -293,7 +292,7 @@ function _M:checkHitOld(atk, def, min, max, factor) return rng.percent(hit), hit end ---Tells the tier difference between two values +--- Applies crossTierEffects according to the tier difference between power and save function _M:crossTierEffect(eff_id, apply_power, apply_save, use_given_e) local q = game.player:hasQuest("tutorial-combat-stats") if q and not q:isCompleted("final-lesson")then @@ -328,7 +327,12 @@ function _M:getTierDiff(atk, def) def = math.floor(def) return math.max(0, math.max(math.ceil(atk/20), 1) - math.max(math.ceil(def/20), 1)) end - +--[[ +--- Gets the duration for crossTier effects based on the tier difference between atk and def +function _M:getTierDiff(atk, def) + return math.floor(math.max(0, self:combatScale(atk - def, 1, 20, 5, 100))) +end +--]] --New, simpler checkHit that relies on rescaleCombatStats() being used elsewhere function _M:checkHit(atk, def, min, max, factor, p) if atk < 0 then atk = 0 end @@ -433,7 +437,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) end local dam, apr, armor = force_dam or self:combatDamage(weapon), self:combatAPR(weapon), target:combatArmor() - print("[ATTACK] to ", target.name, " :: ", dam, apr, armor, atk, "vs.", def, "::", mult) + print("[ATTACK] to ", target.name, "dam/apr/atk/mult ::", dam, apr, atk, mult, "vs. armor/def", armor, def) -- check repel local repelled = false @@ -448,7 +452,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) self:fireTalentCheck("callbackOMeleeAttackBonuses", hd) target, weapon, damtype, mult, dam, apr, atk, def, armor = hd.target, hd.weapon, hd.damtype, hd.mult, hd.dam, hd.apr, hd.atk, hd.def, hd.armor if hd.stop then return end - print("[ATTACK] after melee attack bonus hooks and callbacks :: ", dam, apr, armor, atk, "vs.", def, "::", mult) + print("[ATTACK] after melee attack bonus hooks & callbacks::", dam, apr, atk, mult, "vs. armor/def", armor, def) -- If hit is over 0 it connects, if it is 0 we still have 50% chance local hitted = false @@ -503,6 +507,13 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) self:logCombat(target, "#Target# evades #Source#.") elseif self.turn_procs.auto_melee_hit or (self:checkHit(atk, def) and (self:canSee(target) or self:attr("blind_fight") or target:attr("blind_fighted") or rng.chance(3))) then local pres = util.bound(target:combatArmorHardiness() / 100, 0, 1) + + -- Apply weapon damage range + -- By doing this first, variable damage is more "smooth" against high armor + local damrange = self:combatDamageRange(weapon) + dam = rng.range(dam, dam * damrange) + print("[ATTACK] HIT:: damrange", damrange, "==> dam/apr::", dam, apr, "vs. armor/hardiness", armor, pres) + local eff = target.knowTalent and target:hasEffect(target.EFF_PARRY) -- check if target deflects the blow (deflected blows cannot crit) if eff then @@ -515,28 +526,24 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) end if target.knowTalent and target:hasEffect(target.EFF_GESTURE_OF_GUARDING) and not target:attr("encased_in_ice") then - local deflect = math.min(dam, target:callTalent(target.T_GESTURE_OF_GUARDING, "doGuard")) or 0 - if deflect > 0 then - game:delayedLogDamage(self, target, 0, ("%s(%d gestured#LAST#)"):format(DamageType:get(damtype).text_color or "#aaaaaa#", deflect), false) - dam = dam - deflect + local g_deflect = math.min(dam, target:callTalent(target.T_GESTURE_OF_GUARDING, "doGuard")) or 0 + if g_deflect > 0 then + game:delayedLogDamage(self, target, 0, ("%s(%d gestured#LAST#)"):format(DamageType:get(damtype).text_color or "#aaaaaa#", g_deflect), false) + dam = dam - g_deflect; deflect = deflect + g_deflect end print("[ATTACK] after GESTURE_OF_GUARDING", dam) end if self:isAccuracyEffect(weapon, "knife") then local bonus = 1 + self:getAccuracyEffect(weapon, atk, def, 0.005, 0.25) - print("[ATTACK] dagger accuracy bonus", atk, def, "=", bonus, "previous", apr) apr = apr * bonus + print("[ATTACK] dagger accuracy bonus", atk, def, "=", bonus, "apr ==>", apr) end - print("[ATTACK] raw dam", dam, "versus", armor, pres, "with APR", apr) armor = math.max(0, armor - apr) dam = math.max(dam * pres - armor, 0) + (dam * (1 - pres)) print("[ATTACK] after armor", dam) - local damrange = self:combatDamageRange(weapon) - dam = rng.range(dam, dam * damrange) - print("[ATTACK] after range", dam) - + if deflect == 0 then dam, crit = self:physicalCrit(dam, weapon, target, atk, def) end print("[ATTACK] after crit", dam) dam = dam * mult @@ -787,7 +794,7 @@ function _M:attackTargetHitProcs(target, weapon, dam, apr, armor, damtype, mult, if dam > 0 and self:attr("damage_backfire") then local hurt = math.min(dam, old_target_life) * self.damage_backfire / 100 if hurt > 0 then - self:takeHit(hurt, self) + self:takeHit(hurt, self, {cant_die=true}) end end @@ -1281,13 +1288,19 @@ function _M:combatArmor() if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then add = add + self:callTalent(self.T_ARMOUR_OF_SHADOWS,"ArmourBonus") end + local light_armor = self:hasLightArmor() + if light_armor then + if self:knowTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE) then + add = add + self:callTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE, "getArmour") + end + end if self:knowTalent(self.T_CORRUPTED_SHELL) then add = add + self:getCon() / 3.5 end if self:knowTalent(self.T_CARBON_SPIKES) and self:isTalentActive(self.T_CARBON_SPIKES) then add = add + self.carbon_armor end - if self:knowTalent(self["T_RESHAPE_WEAPON/ARMOUR"]) then add = add + self:callTalent(self["T_RESHAPE_WEAPON/ARMOUR"], "getArmorBoost") end + if self:knowTalent(self["T_FORM_AND_FUNCTION"]) then add = add + self:callTalent(self["T_FORM_AND_FUNCTION"], "getArmorBoost") end return self.combat_armor + add end @@ -1297,9 +1310,6 @@ end function _M:combatArmorHardiness() local add = 0 local multi = 1 - if self:knowTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE) then - add = add + self:callTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE, "getHardiness") - end if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING) add = add + at.getArmorHardiness(self, at) @@ -1316,6 +1326,9 @@ function _M:combatArmorHardiness() if self:knowTalent(self.T_LIGHT_ARMOUR_TRAINING) then add = add + self:callTalent(self.T_LIGHT_ARMOUR_TRAINING, "getArmorHardiness") end + if self:knowTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE) then + add = add + self:callTalent(self.T_SKIRMISHER_BUCKLER_EXPERTISE, "getArmorHardiness") + end end if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then add = add + 50 @@ -1332,7 +1345,7 @@ function _M:combatAttackBase(weapon, ammo) local talent = self:callTalent(self.T_WEAPON_COMBAT, "getAttack") local atk = 4 + self.combat_atk + talent + (weapon.atk or 0) + (ammo and ammo.atk or 0) + (self:getLck() - 50) * 0.4 - if self:knowTalent(self["T_RESHAPE_WEAPON/ARMOUR"]) then atk = atk + self:callTalent(self["T_RESHAPE_WEAPON/ARMOUR"], "getDamBoost", weapon) end + if self:knowTalent(self["T_FORM_AND_FUNCTION"]) then atk = atk + self:callTalent(self["T_FORM_AND_FUNCTION"], "getDamBoost", weapon) end if self:attr("hit_penalty_2h") then atk = atk * (1 - math.max(0, 20 - (self.size_category - 4) * 5) / 100) end @@ -1660,9 +1673,10 @@ function _M:combatDamage(weapon, adddammod, damage) totstat = totstat + self:getStat(stat) * mod end end + if self:knowTalent(self["T_FORM_AND_FUNCTION"]) then totstat = totstat + self:callTalent(self["T_FORM_AND_FUNCTION"], "getDamBoost", weapon) end local talented_mod = 1 + self:combatTrainingPercentInc(weapon) - local power = self:combatDamagePower(damage or weapon) - return self:rescaleDamage(0.3*(self:combatPhysicalpower(nil, weapon) + totstat) * power * talented_mod) + local power = self:combatDamagePower(damage or weapon, totstat) + return self:rescaleDamage(0.3*self:combatPhysicalpower(nil, weapon, totstat) * power * talented_mod) end --- Gets the 'power' portion of the damage @@ -1670,8 +1684,6 @@ function _M:combatDamagePower(weapon_combat, add) if not weapon_combat then return 1 end local power = math.max((weapon_combat.dam or 1) + (add or 0), 1) - if self:knowTalent(self["T_RESHAPE_WEAPON/ARMOUR"]) then power = power + self:callTalent(self["T_RESHAPE_WEAPON/ARMOUR"], "getDamBoost", weapon_combat) end - return (math.sqrt(power / 10) - 1) * 0.5 + 1 end @@ -1809,7 +1821,7 @@ function _M:combatFatigue() local min = self.min_fatigue or 0 local fatigue = self.fatigue - if self:knowTalent(self["T_RESHAPE_WEAPON/ARMOUR"]) then fatigue = fatigue - self:callTalent(self["T_RESHAPE_WEAPON/ARMOUR"], "getFatigueBoost") end + if self:knowTalent(self["T_FORM_AND_FUNCTION"]) then fatigue = fatigue - self:callTalent(self["T_FORM_AND_FUNCTION"], "getFatigueBoost") end if self:knowTalent(self.T_LIGHT_ARMOUR_TRAINING) then fatigue = fatigue - self:callTalent(self.T_LIGHT_ARMOUR_TRAINING, "getFatigue") diff --git a/game/modules/tome/class/interface/PlayerDumpJSON.lua b/game/modules/tome/class/interface/PlayerDumpJSON.lua index f9097e94e46c38e53c68344a8fbb52e1dde69e97..e1d496556c6c94af3b46f82805efc366cf895373 100644 --- a/game/modules/tome/class/interface/PlayerDumpJSON.lua +++ b/game/modules/tome/class/interface/PlayerDumpJSON.lua @@ -87,17 +87,13 @@ function _M:dumpToJSON(js, bypass, nosub) ------------------------------------------------------------------- local r = js:newSection("resources") r.life = string.format("%d/%d", self.life, self.max_life) - if self:knowTalent(self.T_STAMINA_POOL) then r.stamina=string.format("%d/%d", self.stamina, self.max_stamina) end - if self:knowTalent(self.T_MANA_POOL) then r.mana=string.format("%d/%d", self.mana, self.max_mana) end - if self:knowTalent(self.T_SOUL_POOL) then r.souls=string.format("%d/%d", self.soul, self.max_soul) end - if self:knowTalent(self.T_POSITIVE_POOL) then r.positive=string.format("%d/%d", self.positive, self.max_positive) end - if self:knowTalent(self.T_NEGATIVE_POOL) then r.negative=string.format("%d/%d", self.negative, self.max_negative) end - if self:knowTalent(self.T_VIM_POOL) then r.vim=string.format("%d/%d", self.vim, self.max_vim) end - if self:knowTalent(self.T_PSI_POOL) then r.psi=string.format("%d/%d", self.psi, self.max_psi) end - if self.psionic_feedback_max then r.psi_feedback=string.format("%d/%d", self:getFeedback(), self:getMaxFeedback()) end - if self:knowTalent(self.T_EQUILIBRIUM_POOL) then r.equilibrium=string.format("%d", self.equilibrium) end - if self:knowTalent(self.T_PARADOX_POOL) then r.paradox=string.format("%d", self.paradox) end - if self:knowTalent(self.T_HATE_POOL) then r.hate=string.format("%d/%d", self.hate, self.max_hate) end + for res, res_def in ipairs(self.resources_def) do if res_def.talent and self:knowTalent(res_def.talent) then + if res_def.invert_values then + r[res_def.short_name] = string.format("%d", self[res_def.getFunction](self)) + else + r[res_def.short_name] = string.format("%d/%d", self[res_def.getFunction](self), self[res_def.getMaxFunction](self)) + end + end end ------------------------------------------------------------------- -- Inscriptions @@ -108,7 +104,7 @@ function _M:dumpToJSON(js, bypass, nosub) for i = 1, self.max_inscriptions do if self.inscriptions[i] then local t = self:getTalentFromId("T_"..self.inscriptions[i]) local desc = tostring(self:getTalentFullDescription(t)) - ins[#ins+1] = {name=t.name, kind=t.type[1], desc=desc} + ins[#ins+1] = {name=t.name, image=t.image, kind=t.type[1], desc=desc} end end ------------------------------------------------------------------- @@ -237,7 +233,7 @@ function _M:dumpToJSON(js, bypass, nosub) c.damage = {} if self.inc_damage.all then c.damage.all = string.format("%d%%", self.inc_damage.all) end - for i, t in ipairs(DamageType.dam_def) do + for i, t in pairs(DamageType.dam_def) do if self:combatHasDamageIncrease(DamageType[t.type]) then c.damage[t.name] = string.format("%d%%", self:combatGetDamageIncrease(DamageType[t.type])) end @@ -246,7 +242,7 @@ function _M:dumpToJSON(js, bypass, nosub) c.damage_pen = {} if self.resists_pen.all then c.damage_pen.all = string.format("%d%%", self.resists_pen.all) end - for i, t in ipairs(DamageType.dam_def) do + for i, t in pairs(DamageType.dam_def) do if self.resists_pen[DamageType[t.type]] and self.resists_pen[DamageType[t.type]] ~= 0 then c.damage_pen[t.name] = string.format("%d%%", self.resists_pen[DamageType[t.type]] + (self.resists_pen.all or 0)) end @@ -268,7 +264,7 @@ function _M:dumpToJSON(js, bypass, nosub) d.resistances = {} if self.resists.all then d.resistances.all = string.format("%3d%%(%3d%%)", self.resists.all, self.resists_cap.all or 0) end - for i, t in ipairs(DamageType.dam_def) do + for i, t in pairs(DamageType.dam_def) do if self.resists[DamageType[t.type]] and self.resists[DamageType[t.type]] ~= 0 then d.resistances[t.name] = string.format("%3d%%(%3d%%)", self:combatGetResist(DamageType[t.type]), (self.resists_cap[DamageType[t.type]] or 0) + (self.resists_cap.all or 0)) end @@ -307,7 +303,7 @@ function _M:dumpToJSON(js, bypass, nosub) if not t.hide then local skillname = t.name local desc = self:getTalentFullDescription(t):toString() - td.list[#td.list+1] = { name=skillname, val=("%d/%d"):format(self:getTalentLevelRaw(t.id), t.points), desc=desc} + td.list[#td.list+1] = { name=skillname, image=t.image, val=("%d/%d"):format(self:getTalentLevelRaw(t.id), t.points), desc=desc} end end end @@ -367,7 +363,7 @@ function _M:dumpToJSON(js, bypass, nosub) local desc = tostring(o:getDesc()) equip[self.inven_def[inven_id].name] = equip[self.inven_def[inven_id].name] or {} local ie = equip[self.inven_def[inven_id].name] - ie[#ie+1] = { name=o:getName{do_color=true, no_image=true}, desc=desc } + ie[#ie+1] = { name=o:getName{do_color=true, no_image=true}, image=o.image, desc=desc } end end end @@ -378,7 +374,7 @@ function _M:dumpToJSON(js, bypass, nosub) local inven = js:newSection("inventory") for item, o in ipairs(self.inven[self.INVEN_INVEN]) do local desc = tostring(o:getDesc()) - inven[#inven+1] = { name=o:getName{do_color=true, no_image=true}, desc=desc } + inven[#inven+1] = { name=o:getName{do_color=true, no_image=true}, image=o.image, desc=desc } end ------------------------------------------------------------------- @@ -424,6 +420,12 @@ function _M:dumpToJSON(js, bypass, nosub) if self.has_custom_tile then tags.tile = self.has_custom_tile js:hiddenData("tile", self.has_custom_tile) + elseif self.moddable_tile then + local doll = { self.image } + for i, mo in ipairs(self.add_mos or {}) do + doll[#doll+1] = mo.image + end + js:hiddenData("doll", doll) end self:triggerHook{"ToME:PlayerDumpJSON", title=title, js=js, tags=tags} diff --git a/game/modules/tome/class/interface/PlayerStats.lua b/game/modules/tome/class/interface/PlayerStats.lua index 9508c7f4536a5050fb575a2a3d2e37121a63851f..a020464f064e3d1179a48bfc5ce8faf570749ed3 100644 --- a/game/modules/tome/class/interface/PlayerStats.lua +++ b/game/modules/tome/class/interface/PlayerStats.lua @@ -29,13 +29,13 @@ end function _M:registerDeath(src) local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true}) local name = src.name - profile:saveModuleProfile("deaths", {source=name, cid=pid, nb={"inc",1}}) + profile:incrDataProfile("deaths", {source=name, cid=pid, nb=1}) end function _M:registerUniqueKilled(who) local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true}) - profile:saveModuleProfile("uniques", {victim=who.name, cid=pid, nb={"inc",1}}) + profile:incrDataProfile("uniques", {victim=who.name, cid=pid, nb=1}) end function _M:registerArtifactsPicked(what) @@ -44,13 +44,13 @@ function _M:registerArtifactsPicked(what) local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true}) local name = what:getName{do_color=false, do_count=false, force_id=true, no_add_name=true} - profile:saveModuleProfile("artifacts", {name=name, cid=pid, nb={"inc",1}}) + profile:incrDataProfile("artifacts", {name=name, cid=pid, nb=1}) end function _M:registerCharacterPlayed() local pid = self:playerStatGetCharacterIdentifier(game.party:findMember{main=true}) - profile:saveModuleProfile("characters", {cid=pid, nb={"inc",1}}) + profile:incrDataProfile("characters", {cid=pid, nb=1}) end function _M:registerLoreFound(lore) diff --git a/game/modules/tome/class/uiset/Minimalist.lua b/game/modules/tome/class/uiset/Minimalist.lua index e3b09c0a576dec8d5b19472afa06fc0f9c0065c9..44fea916f7740bdc41d095bd270e9d1c19986394 100644 --- a/game/modules/tome/class/uiset/Minimalist.lua +++ b/game/modules/tome/class/uiset/Minimalist.lua @@ -779,9 +779,9 @@ function _M:displayResources(scale, bx, by, a) end local shield, max_shield = 0, 0 - if player:attr("time_shield") then shield = shield + player.time_shield_absorb max_shield = max_shield + player.time_shield_absorb_max end - if player:attr("damage_shield") then shield = shield + player.damage_shield_absorb max_shield = max_shield + player.damage_shield_absorb_max end - if player:attr("displacement_shield") then shield = shield + player.displacement_shield max_shield = max_shield + player.displacement_shield_max end + if player:attr("time_shield") then shield = shield + (player.time_shield_absorb or 0) max_shield = max_shield + (player.time_shield_absorb_max or 0) end + if player:attr("damage_shield") then shield = shield + (player.damage_shield_absorb or 0) max_shield = max_shield + (player.damage_shield_absorb_max or 0) end + if player:attr("displacement_shield") then shield = shield + (player.displacement_shield or 0) max_shield = max_shield + (player.displacement_shield_max or 0) end local front = fshat_life_dark if max_shield > 0 then diff --git a/game/modules/tome/data/birth/classes/afflicted.lua b/game/modules/tome/data/birth/classes/afflicted.lua index 260e9f932fd1e500423f5736520bfcac39c6dc24..8c944c34230d98eb0a2f9b6a4a8a85f6f05c4846 100644 --- a/game/modules/tome/data/birth/classes/afflicted.lua +++ b/game/modules/tome/data/birth/classes/afflicted.lua @@ -148,6 +148,10 @@ newBirthDescriptor{ }, copy = { max_life = 90, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="mindstar"}, + OFFHAND = {type="weapon", subtype="mindstar"}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, diff --git a/game/modules/tome/data/birth/classes/celestial.lua b/game/modules/tome/data/birth/classes/celestial.lua index 7bdf42f8b3dfd68660c1ed455656acf93048443d..b3b932ba9c40f4042940af76c5ee5fad93914be1 100644 --- a/game/modules/tome/data/birth/classes/celestial.lua +++ b/game/modules/tome/data/birth/classes/celestial.lua @@ -49,6 +49,11 @@ newBirthDescriptor{ }, } +local shield_special = function(e) -- allows any object with shield combat + local combat = e.shield_normal_combat and e.combat or e.special_combat + return combat and combat.block +end + newBirthDescriptor{ type = "subclass", name = "Sun Paladin", @@ -91,6 +96,11 @@ newBirthDescriptor{ }, copy = { max_life = 110, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", not_properties={"twohanded"}}, + OFFHAND = {special=shield_special}, + BODY = {type="armor", special=function(e) return e.subtype=="heavy" or e.subtype=="massive" end}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="mace", name="iron mace", autoreq=true, ego_chance=-1000}, {type="armor", subtype="shield", name="iron shield", autoreq=true, ego_chance=-1000}, @@ -145,9 +155,20 @@ newBirthDescriptor{ }, copy = { max_life = 90, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="staff"}, + OFFHAND = {special=function(e, filter) -- only allow if there is a 1H weapon in MAINHAND + local who = filter._equipping_entity + if who then + local mh = who:getInven(who.INVEN_MAINHAND) mh = mh and mh[1] + if mh and (not mh.slot_forbid or not who:slotForbidCheck(e, who.INVEN_MAINHAND)) then return true end + end + return false + end} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, - {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000} + {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000}, }, }, } diff --git a/game/modules/tome/data/birth/classes/chronomancer.lua b/game/modules/tome/data/birth/classes/chronomancer.lua index 8034512d9a91fc7f6e6ab038febbfad1b5ccc5ed..fec5c184f2b9995ae4bc0a9d0b4e7b67abc8b846 100644 --- a/game/modules/tome/data/birth/classes/chronomancer.lua +++ b/game/modules/tome/data/birth/classes/chronomancer.lua @@ -127,6 +127,17 @@ newBirthDescriptor{ }, copy = { max_life = 90, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="staff"}, + OFFHAND = {special=function(e, filter) -- only allow if there is a 1H weapon in MAINHAND + local who = filter._equipping_entity + if who then + local mh = who:getInven(who.INVEN_MAINHAND) mh = mh and mh[1] + if mh and (not mh.slot_forbid or not who:slotForbidCheck(e, who.INVEN_MAINHAND)) then return true end + end + return false + end} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000}, @@ -200,6 +211,15 @@ newBirthDescriptor{ }, copy = { max_life = 100, + resolvers.auto_equip_filters{MAINHAND = {type="weapon", subtype="longbow"}, + OFFHAND = {type="none"}, + QUIVER={properties={"archery_ammo"}, special=function(e, filter) -- must match the MAINHAND weapon, if any + local mh = filter._equipping_entity and filter._equipping_entity:getInven(filter._equipping_entity.INVEN_MAINHAND) + mh = mh and mh[1] + if not mh or mh.archery == e.archery_ammo then return true end + end}, + QS_MAINHAND = {type="weapon", not_properties={"twohanded"}}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="longbow", name="elm longbow", autoreq=true, ego_chance=-1000}, {type="ammo", subtype="arrow", name="quiver of elm arrows", autoreq=true, ego_chance=-1000}, diff --git a/game/modules/tome/data/birth/classes/corrupted.lua b/game/modules/tome/data/birth/classes/corrupted.lua index 97f4fc27233f3ed9b38953f8dd500a5d99ab7a39..4d269a43aff2957911e9b5964ce7e895243ef3ee 100644 --- a/game/modules/tome/data/birth/classes/corrupted.lua +++ b/game/modules/tome/data/birth/classes/corrupted.lua @@ -82,6 +82,10 @@ newBirthDescriptor{ [ActorTalents.T_REND] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", not_properties={"twohanded"}}, + OFFHAND = {type="weapon", not_properties={"twohanded"}} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="waraxe", name="iron waraxe", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="waraxe", name="iron waraxe", autoreq=true, ego_chance=-1000}, @@ -135,6 +139,17 @@ newBirthDescriptor{ [ActorTalents.T_PACIFICATION_HEX] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="staff"}, + OFFHAND = {special=function(e, filter) -- only allow if there is a 1H weapon in MAINHAND + local who = filter._equipping_entity + if who then + local mh = who:getInven(who.INVEN_MAINHAND) mh = mh and mh[1] + if mh and (not mh.slot_forbid or not who:slotForbidCheck(e, who.INVEN_MAINHAND)) then return true end + end + return false + end} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000} diff --git a/game/modules/tome/data/birth/classes/mage.lua b/game/modules/tome/data/birth/classes/mage.lua index 1a98d48c1cc021d9aebd2f022469a7125b775d66..8cf927e7241b8e1b64235a072a1ae2a37ff8d029 100644 --- a/game/modules/tome/data/birth/classes/mage.lua +++ b/game/modules/tome/data/birth/classes/mage.lua @@ -19,6 +19,18 @@ local Particles = require "engine.Particles" +local mage_equip_filters = resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="staff"}, + OFFHAND = {special=function(e, filter) -- only allow if there is a 1H weapon in MAINHAND + local who = filter._equipping_entity + if who then + local mh = who:getInven(who.INVEN_MAINHAND) mh = mh and mh[1] + if mh and (not mh.slot_forbid or not who:slotForbidCheck(e, who.INVEN_MAINHAND)) then return true end + end + return false + end} +}, + newBirthDescriptor{ type = "class", name = "Mage", @@ -87,6 +99,8 @@ newBirthDescriptor{ }, copy = { max_life = 90, + mage_equip_filters, + resolvers.auto_equip_filters{QUIVER = {type="alchemist-gem"}}, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000} @@ -213,6 +227,7 @@ newBirthDescriptor{ end, max_life = 90, + mage_equip_filters, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000}, @@ -279,9 +294,9 @@ newBirthDescriptor{ copy = { soul = 1, max_life = 90, + mage_equip_filters, resolvers.equipbirth{ id=true, {type="weapon", subtype="staff", name="elm staff", autoreq=true, ego_chance=-1000}, --- {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, {type="armor", subtype="cloth", name="linen robe", autoreq=true, ego_chance=-1000}, }, }, diff --git a/game/modules/tome/data/birth/classes/rogue.lua b/game/modules/tome/data/birth/classes/rogue.lua index f6636868b0964deac3e0c7d8b82ec4519aa7cc9b..6a8e39214f35949c81128cc4416cd4ec4cb0694e 100644 --- a/game/modules/tome/data/birth/classes/rogue.lua +++ b/game/modules/tome/data/birth/classes/rogue.lua @@ -83,6 +83,11 @@ newBirthDescriptor{ [ActorTalents.T_WEAPON_COMBAT] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="dagger"}, + OFFHAND = {type="weapon", subtype="dagger"}, + BODY = {type="armor", special=function(e) return e.subtype=="light" or e.subtype=="cloth" end}, + }, equipment = resolvers.equipbirth{ id=true, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, @@ -147,6 +152,11 @@ newBirthDescriptor{ }, copy = { resolvers.inscription("RUNE:_MANASURGE", {cooldown=25, dur=10, mana=620}), + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="dagger"}, + OFFHAND = {type="weapon", subtype="dagger"}, + BODY = {type="armor", special=function(e) return e.subtype=="light" or e.subtype=="cloth" end}, + }, equipment = resolvers.equipbirth{ id=true, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, @@ -197,6 +207,10 @@ newBirthDescriptor{ [ActorTalents.T_ARMOUR_TRAINING] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="dagger"}, + OFFHAND = {type="weapon", subtype="dagger"} + }, equipment = resolvers.equipbirth{ id=true, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="dagger", name="iron dagger", autoreq=true, ego_chance=-1000}, @@ -206,6 +220,11 @@ newBirthDescriptor{ }, } +local shield_special = function(e) -- allows any object with shield combat + local combat = e.shield_normal_combat and e.combat or e.special_combat + return combat and combat.block +end + newBirthDescriptor{ type = "subclass", name = "Skirmisher", @@ -249,11 +268,20 @@ newBirthDescriptor{ [ActorTalents.T_WEAPON_COMBAT] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="sling"}, + OFFHAND = {special=shield_special}, + QUIVER={properties={"archery_ammo"}, special=function(e, filter) -- must match the MAINHAND weapon, if any + local mh = filter._equipping_entity and filter._equipping_entity:getInven(filter._equipping_entity.INVEN_MAINHAND) + mh = mh and mh[1] + if not mh or mh.archery == e.archery_ammo then return true end + end} + }, resolvers.equipbirth{ id=true, {type="armor", subtype="light", name="rough leather armour", autoreq=true,ego_chance=-1000}, {type="weapon", subtype="sling", name="rough leather sling", autoreq=true, ego_chance=-1000}, - {type="armor", subtype="shield", name="iron shield", autoreq=false, ego_chance=-1000, ego_chance=-1000}, + {type="armor", subtype="shield", name="iron shield", autoreq=true, ego_chance=-1000}, {type="ammo", subtype="shot", name="pouch of iron shots", autoreq=true, ego_chance=-1000}, }, resolvers.generic(function(e) diff --git a/game/modules/tome/data/birth/classes/warrior.lua b/game/modules/tome/data/birth/classes/warrior.lua index 6ca9281ccdaf50d7d8dd31de61f5dc7e50a0dda3..33106c0de89b4bb0b6a1e08ea455b615bd86f297 100644 --- a/game/modules/tome/data/birth/classes/warrior.lua +++ b/game/modules/tome/data/birth/classes/warrior.lua @@ -84,6 +84,10 @@ newBirthDescriptor{ [ActorTalents.T_ARMOUR_TRAINING] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", properties={"twohanded"}}, + OFFHAND = {type="none"} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="greatsword", name="iron greatsword", autoreq=true, ego_chance=-1000, ego_chance=-1000}, {type="armor", subtype="heavy", name="iron mail armour", autoreq=true, ego_chance=-1000, ego_chance=-1000}, @@ -94,6 +98,11 @@ newBirthDescriptor{ }, } +local shield_special = function(e) -- allows any object with shield combat + local combat = e.shield_normal_combat and e.combat or e.special_combat + return combat and combat.block +end + newBirthDescriptor{ type = "subclass", name = "Bulwark", @@ -137,6 +146,11 @@ newBirthDescriptor{ [ActorTalents.T_WEAPONS_MASTERY] = 1, }, copy = { + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", not_properties={"twohanded"}}, + OFFHAND = {special=shield_special}, + BODY = {type="armor", special=function(e) return e.subtype=="heavy" or e.subtype=="massive" end}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="longsword", name="iron longsword", autoreq=true, ego_chance=-1000, ego_chance=-1000}, {type="armor", subtype="shield", name="iron shield", autoreq=true, ego_chance=-1000, ego_chance=-1000}, @@ -188,6 +202,13 @@ newBirthDescriptor{ }, copy = { max_life = 110, + resolvers.auto_equip_filters{MAINHAND = {type="weapon", properties={"archery"}}, + QUIVER={properties={"archery_ammo"}, special=function(e, filter) -- must match the MAINHAND weapon, if any + local mh = filter._equipping_entity and filter._equipping_entity:getInven(filter._equipping_entity.INVEN_MAINHAND) + mh = mh and mh[1] + if not mh or mh.archery == e.archery_ammo then return true end + end} + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="longbow", name="elm longbow", autoreq=true, ego_chance=-1000}, {type="ammo", subtype="arrow", name="quiver of elm arrows", autoreq=true, ego_chance=-1000}, @@ -317,6 +338,9 @@ newBirthDescriptor{ [ActorTalents.T_UNARMED_MASTERY] = 1, -- early game is absolutely stupid without this }, copy = { + resolvers.auto_equip_filters{-- will not try to equip weapons + MAINHAND = {type="none"}, OFFHAND = {type="none"} + }, resolvers.equipbirth{ id=true, {type="armor", subtype="hands", name="iron gauntlets", autoreq=true, ego_chance=-1000, ego_chance=-1000}, {type="armor", subtype="light", name="rough leather armour", autoreq=true, ego_chance=-1000, ego_chance=-1000}, @@ -330,5 +354,3 @@ newBirthDescriptor{ }, } - - diff --git a/game/modules/tome/data/birth/classes/wilder.lua b/game/modules/tome/data/birth/classes/wilder.lua index ad921c757478c56ad38ba59f7c801b5eff616bd7..84251c9cf4fb86a1ed2fb7f57d1674bb7c89dc21 100644 --- a/game/modules/tome/data/birth/classes/wilder.lua +++ b/game/modules/tome/data/birth/classes/wilder.lua @@ -94,6 +94,10 @@ newBirthDescriptor{ }, copy = { max_life = 90, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="mindstar"}, + OFFHAND = {type="weapon", subtype="mindstar"}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, @@ -137,11 +141,11 @@ newBirthDescriptor{ ["wild-gift/higher-draconic"]={false, 0.3}, ["wild-gift/fungus"]={true, 0.1}, ["cunning/survival"]={false, 0}, - ["technique/shield-offense"]={true, 0.1}, - ["technique/2hweapon-assault"]={true, 0.1}, - ["technique/combat-techniques-active"]={false, 0}, - ["technique/combat-techniques-passive"]={true, 0}, - ["technique/combat-training"]={true, 0}, + ["technique/shield-offense"]={true, 0.2}, + ["technique/2hweapon-assault"]={true, 0.2}, + ["technique/combat-techniques-active"]={false, 0.2}, + ["technique/combat-techniques-passive"]={true, 0.2}, + ["technique/combat-training"]={true, 0.3}, }, talents = { [ActorTalents.T_ICE_CLAW] = 1, @@ -220,6 +224,10 @@ newBirthDescriptor{ copy = { forbid_arcane = 2, max_life = 90, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="mindstar"}, + OFFHAND = {type="weapon", subtype="mindstar"}, + }, resolvers.equipbirth{ id=true, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, {type="weapon", subtype="mindstar", name="mossy mindstar", autoreq=true, ego_chance=-1000}, @@ -231,6 +239,11 @@ newBirthDescriptor{ }, } +local shield_special = function(e) -- allows any object with shield combat + local combat = e.shield_normal_combat and e.combat or e.special_combat + return combat and combat.block +end + newBirthDescriptor{ type = "subclass", name = "Stone Warden", @@ -275,6 +288,10 @@ newBirthDescriptor{ }, copy = { max_life = 110, + resolvers.auto_equip_filters{ + MAINHAND = {special=shield_special}, + OFFHAND = {special=shield_special} + }, resolvers.equipbirth{ id=true, {type="armor", subtype="shield", name="iron shield", autoreq=true, ego_chance=-1000, ego_chance=-1000}, {type="armor", subtype="shield", name="iron shield", autoreq=true, ego_chance=-1000, ego_chance=-1000}, diff --git a/game/modules/tome/data/birth/races/dwarf.lua b/game/modules/tome/data/birth/races/dwarf.lua index 5181a98b31877df95a3fc73bd84d2bce06af1b4a..76436f42e2ba7278cdf8e1837915f4695ffbbd01 100644 --- a/game/modules/tome/data/birth/races/dwarf.lua +++ b/game/modules/tome/data/birth/races/dwarf.lua @@ -52,29 +52,6 @@ newBirthDescriptor{ random_escort_possibilities = { {"tier1.1", 1, 2}, {"tier1.2", 1, 2}, {"daikara", 1, 2}, {"old-forest", 1, 4}, {"dreadfell", 1, 8}, {"reknor", 1, 2}, }, moddable_attachement_spots = "race_dwarf", - cosmetic_unlock = { - cosmetic_race_dwarf_female_beard = { - {priority=2, name="Beard [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="beard_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - {priority=2, name="Sideburns [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="sideburners_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - {priority=2, name="Mustache [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="mustache_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - {priority=2, name="Flip [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="flip_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - {priority=2, name="Donut [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="donut_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - }, - cosmetic_race_human_redhead = { - {priority=1, name="Redhead [donator only]", donator=true, reset=function(actor) actor.is_redhead=false end, on_actor=function(actor) if actor.moddable_tile then actor.is_redhead = true actor.moddable_tile_base = "base_redhead_01.png" actor.moddable_tile_ornament2={male="beard_redhead_02"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Male" end}, - {priority=1, name="Redhead [donator only]", donator=true, reset=function(actor) actor.is_redhead=false end, on_actor=function(actor) if actor.moddable_tile then actor.is_redhead = true actor.is_redhead = true actor.moddable_tile_base = "base_redhead_01.png" actor.moddable_tile_ornament2={female="braid_redhead_01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - }, - cosmetic_bikini = { - {name="Bikini [donator only]", donator=true, on_actor=function(actor, birther, last) - if not last then local o = birther.obj_list_by_name.Bikini if not o then print("No bikini found!") return end actor:getInven(actor.INVEN_BODY)[1] = o:cloneFull() - else actor:registerOnBirthForceWear("FUN_BIKINI") end - end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, - {name="Mankini [donator only]", donator=true, on_actor=function(actor, birther, last) - if not last then local o = birther.obj_list_by_name.Mankini if not o then print("No mankini found!") return end actor:getInven(actor.INVEN_BODY)[1] = o:cloneFull() - else actor:registerOnBirthForceWear("FUN_MANKINI") end - end, check=function(birth) return birth.descriptors_by_type.sex == "Male" end}, - }, - }, } --------------------------------------------------------- @@ -106,4 +83,27 @@ newBirthDescriptor life_rating=12, }, experience = 1.25, + cosmetic_unlock = { + cosmetic_race_dwarf_female_beard = { + {priority=2, name="Beard [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="beard_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + {priority=2, name="Sideburns [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="sideburners_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + {priority=2, name="Mustache [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="mustache_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + {priority=2, name="Flip [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="flip_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + {priority=2, name="Donut [donator only]", donator=true, on_actor=function(actor) if actor.moddable_tile then actor.moddable_tile_ornament={female="donut_"..(actor.is_redhead and "redhead_" or "").."01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + }, + cosmetic_race_human_redhead = { + {priority=1, name="Redhead [donator only]", donator=true, reset=function(actor) actor.is_redhead=false end, on_actor=function(actor) if actor.moddable_tile then actor.is_redhead = true actor.moddable_tile_base = "base_redhead_01.png" actor.moddable_tile_ornament2={male="beard_redhead_02"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Male" end}, + {priority=1, name="Redhead [donator only]", donator=true, reset=function(actor) actor.is_redhead=false end, on_actor=function(actor) if actor.moddable_tile then actor.is_redhead = true actor.is_redhead = true actor.moddable_tile_base = "base_redhead_01.png" actor.moddable_tile_ornament2={female="braid_redhead_01"} end end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + }, + cosmetic_bikini = { + {name="Bikini [donator only]", donator=true, on_actor=function(actor, birther, last) + if not last then local o = birther.obj_list_by_name.Bikini if not o then print("No bikini found!") return end actor:getInven(actor.INVEN_BODY)[1] = o:cloneFull() + else actor:registerOnBirthForceWear("FUN_BIKINI") end + end, check=function(birth) return birth.descriptors_by_type.sex == "Female" end}, + {name="Mankini [donator only]", donator=true, on_actor=function(actor, birther, last) + if not last then local o = birther.obj_list_by_name.Mankini if not o then print("No mankini found!") return end actor:getInven(actor.INVEN_BODY)[1] = o:cloneFull() + else actor:registerOnBirthForceWear("FUN_MANKINI") end + end, check=function(birth) return birth.descriptors_by_type.sex == "Male" end}, + }, + }, } diff --git a/game/modules/tome/data/chats/escort-quest.lua b/game/modules/tome/data/chats/escort-quest.lua index 503f4f9b9cd707247c22887213c797709955c10b..0a1bfd7ea0a266f3ed56fbba4cd74fd6d5f1958b 100644 --- a/game/modules/tome/data/chats/escort-quest.lua +++ b/game/modules/tome/data/chats/escort-quest.lua @@ -150,7 +150,7 @@ local reward_types = { ["psionic/feedback"] = 0.8, }, talents = { - [Talents.T_BIOFEEDBACK] = 1, + [Talents.T_RESONANCE_FIELD] = 1, [Talents.T_CONVERSION] = 1, }, saves = { spell = 4, mental = 4 }, @@ -286,7 +286,7 @@ local function generate_rewards() local tt_def = npc:getTalentTypeFrom(tt) local cat = tt_def.type:gsub("/.*", "") local doit = function(npc, player) game.party:reward("Select the party member to receive the reward:", function(player) - if player:knowTalentType(tt) == nil then player:setTalentTypeMastery(tt, mastery) end + if player:knowTalentType(tt) == nil then player:setTalentTypeMastery(tt, mastery - 1 + player:getTalentTypeMastery(tt)) end player:learnTalentType(tt, false) player:hasQuest(npc.quest_id).reward_message = ("gained talent category %s (at mastery %0.2f)"):format(cat:capitalize().." / "..tt_def.name:capitalize(), mastery) end) end diff --git a/game/modules/tome/data/chats/paradoxology.lua b/game/modules/tome/data/chats/paradoxology.lua index fb6a974819b586217f485cf772aa779f78876a96..d8a372c1c1dd2a7041da8170b88f929794ca01ac 100644 --- a/game/modules/tome/data/chats/paradoxology.lua +++ b/game/modules/tome/data/chats/paradoxology.lua @@ -22,6 +22,7 @@ newChat{ id="welcome", NO! YOU CAN'T! NO GOING THERE! YOU... I... YOU MUST NOT GO THERE! THIS CANNOT BE AVOIDED! I MUST STOP IT! PLEASE DON'T! I MUST KILL MYSELF TO PROTECT MYSELF! +#LIGHT_GREEN#*Before you can react, you... I... yourself vanishes into a rift hanging in midair.*#WHITE# ]], answers = { {"What the..."}, diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua index 6b5b778a0e1ec05f6d516693e4339a52724d8bd3..75c5b173262abeed627b4d762b1c6b678377ec1f 100644 --- a/game/modules/tome/data/damage_types.lua +++ b/game/modules/tome/data/damage_types.lua @@ -1782,7 +1782,7 @@ newDamageType{ projector = function(src, x, y, type, dam, state) state = initState(state) useImplicitCrit(src, state) - if _G.type(dam) == "number" then dam = {dam=dam, dur=3, fail=50*dam.power/(dam.power+50)} end + if _G.type(dam) == "number" then dam = {dam=dam, dur=3, fail=50*dam/(dam+50)} end DamageType:get(DamageType.NATURE).projector(src, x, y, DamageType.NATURE, dam.dam / dam.dur, state) local target = game.level.map(x, y, Map.ACTOR) if target and target:canBe("poison") then diff --git a/game/modules/tome/data/general/encounters/maj-eyal.lua b/game/modules/tome/data/general/encounters/maj-eyal.lua index c94bd51711490af02a187cb063c26d799926b237..34083f323023d20a18b1d2e921c766622bcf7a57 100644 --- a/game/modules/tome/data/general/encounters/maj-eyal.lua +++ b/game/modules/tome/data/general/encounters/maj-eyal.lua @@ -20,7 +20,7 @@ newEntity{ name = "Novice mage", type = "harmless", subtype = "special", unique = true, - immediate = {"world-encounter", "angolwen"}, + immediate = {"world-encounter", "angolwen-quest"}, -- Spawn the novice mage near the player on_encounter = function(self, who) local x, y = self:findSpot(who) diff --git a/game/modules/tome/data/general/events/sub-vault.lua b/game/modules/tome/data/general/events/sub-vault.lua index 06fc24a05d8f00a25ce43f99393fa0656405a844..e1bacc600e58471febedb8ca641f0de66512057f 100644 --- a/game/modules/tome/data/general/events/sub-vault.lua +++ b/game/modules/tome/data/general/events/sub-vault.lua @@ -32,7 +32,6 @@ local changer = function(id) npc_list.__loaded_files = table.clone(npc_list.__loaded_files, true) -- Separate full cloning to not alter the base npc_list.ignore_loaded = true mod.class.NPC:loadList("/data/general/npcs/all.lua", nil, npc_list, function(e) if e.rarity then e.rarity = math.ceil(e.rarity * 35 + 4) end end, npc_list.__loaded_files) - for i = 1, #npc_list do npc_list[i].faction = "enemies" end local walltype = "HARDWALL" if game.level.data.generator and game.level.data.generator.map and game.level.data.generator.map.subvault_exterior_wall then walltype = game.level.data.generator.map.subvault_exterior_wall end @@ -102,6 +101,9 @@ local changer = function(id) grid_list = grid_list, object_list = table.clone(game.zone.object_list, false), trap_list = table.clone(game.zone.trap_list, false), + post_process = function(level) + for uid, e in pairs(level.entities) do e.faction = e.hard_faction or "enemies" end + end, }) return zone end diff --git a/game/modules/tome/data/general/npcs/elven-caster.lua b/game/modules/tome/data/general/npcs/elven-caster.lua index 773d9f792697e85e9f7a3a900e82dcf93d88a185..4a73674135784adf815da72a82b6eabfd3578578 100644 --- a/game/modules/tome/data/general/npcs/elven-caster.lua +++ b/game/modules/tome/data/general/npcs/elven-caster.lua @@ -44,6 +44,7 @@ newEntity{ resolvers.racial(), resolvers.talents{ [Talents.T_ARMOUR_TRAINING]=1, }, + resolvers.auto_equip_filters("Archmage"), autolevel = "caster", ai = "dumb_talented_simple", ai_state = { ai_move="move_complex", talent_in=1, }, diff --git a/game/modules/tome/data/general/npcs/horror.lua b/game/modules/tome/data/general/npcs/horror.lua index 8a07a5ff8715d8c4c078bad48ff10889222e9b29..c4691854b5943ccbf58af6d9b767db0e9ba04b01 100644 --- a/game/modules/tome/data/general/npcs/horror.lua +++ b/game/modules/tome/data/general/npcs/horror.lua @@ -77,7 +77,7 @@ Each swing drips pustulant fluid before it, and each droplet writhes and wriggle resolvers.equip{ {type="weapon", subtype="waraxe", ego_chance = 100, autoreq=true}, {type="weapon", subtype="waraxe", ego_chance = 100, autoreq=true}, - {type="armor", subtype="robe", ego_chance = 100, autoreq=true} + {type="armor", subtype="cloth", ego_chance = 100, autoreq=true} }, talent_cd_reduction = {[Talents.T_BLINDSIDE]=4}, diff --git a/game/modules/tome/data/general/npcs/major-demon.lua b/game/modules/tome/data/general/npcs/major-demon.lua index 80557c28bd20565d488576318b39c270dcb8471e..3390782f6662d788a9341e4bdc4414b33e1ebd50 100644 --- a/game/modules/tome/data/general/npcs/major-demon.lua +++ b/game/modules/tome/data/general/npcs/major-demon.lua @@ -177,6 +177,7 @@ It moves swiftly toward you, casting terrible spells and swinging its weapons at ai = "tactical", + resolvers.auto_equip_filters("Reaver"), resolvers.equip{ {type="weapon", subtype="longsword", forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.equip{ {type="weapon", subtype="waraxe", forbid_power_source={antimagic=true}, autoreq=true}, }, @@ -219,6 +220,7 @@ newEntity{ base = "BASE_NPC_MAJOR_DEMON", combat_dam = resolvers.levelup(resolvers.mbonus(40, 20), 1, 2), + resolvers.auto_equip_filters{MAINHAND = {properties = {"twohanded"}}, }, resolvers.equip{ {type="weapon", subtype="greatsword", autoreq=true}, }, resists={all = resolvers.mbonus(25, 20)}, @@ -259,6 +261,7 @@ newEntity{ base = "BASE_NPC_MAJOR_DEMON", ai = "tactical", + resolvers.auto_equip_filters("Reaver"), resolvers.equip{ {type="weapon", subtype="mace", forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.equip{ {type="weapon", subtype="mace", forbid_power_source={antimagic=true}, autoreq=true}, }, @@ -306,6 +309,7 @@ newEntity{ base = "BASE_NPC_MAJOR_DEMON", ai = "tactical", + resolvers.auto_equip_filters{MAINHAND = {properties = {"twohanded"}}, }, resolvers.equip{ {type="weapon", subtype="battleaxe", defined="KHULMANAR_WRATH", random_art_replace={chance=30}, autoreq=true, force_drop=true}, }, resists={[DamageType.PHYSICAL] = resolvers.mbonus(8, 8), [DamageType.FIRE] = 100}, diff --git a/game/modules/tome/data/general/npcs/minor-demon.lua b/game/modules/tome/data/general/npcs/minor-demon.lua index 9f3bf5e544c54ec425db1b12db0479ab926931f4..6a7a7fda40b9f7185bc262277837455719c4b704 100644 --- a/game/modules/tome/data/general/npcs/minor-demon.lua +++ b/game/modules/tome/data/general/npcs/minor-demon.lua @@ -106,6 +106,7 @@ newEntity{ base = "BASE_NPC_DEMON", [Talents.T_OVERPOWER]={base=1, every=6, max=5}, [Talents.T_RUSH]=6, }, + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ {type="weapon", subtype="longsword", autoreq=true}, {type="armor", subtype="shield", autoreq=true}, diff --git a/game/modules/tome/data/general/npcs/minotaur.lua b/game/modules/tome/data/general/npcs/minotaur.lua index ea75a063a366ee260634dde3c0101d3820b48b79..890c798f8e259edff56a2befcf0e3506a7a8b3db 100644 --- a/game/modules/tome/data/general/npcs/minotaur.lua +++ b/game/modules/tome/data/general/npcs/minotaur.lua @@ -17,8 +17,6 @@ -- Nicolas Casalini "DarkGod" -- darkgod@te4.org --- last updated: 11:56 AM 2/5/2010 - local Talents = require("engine.interface.ActorTalents") newEntity{ @@ -60,6 +58,7 @@ newEntity{ base = "BASE_NPC_MINOTAUR", name = "minotaur", color=colors.UMBER, resolvers.nice_tile{image="invis.png", add_mos = {{image="npc/giant_minotaur_minotaur.png", display_h=2, display_y=-1}}}, desc = [[It is a cross between a human and a bull.]], + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", autoreq=true}, }, level_range = {10, nil}, exp_worth = 1, rarity = 1, @@ -79,7 +78,8 @@ newEntity{ base = "BASE_NPC_MINOTAUR", level_range = {20, nil}, exp_worth = 1, rarity = 4, combat_armor = 15, combat_def = 7, - resolvers.equip{ {type="weapon", subtype="maul", forbid_power_source={antimagic=true}, autoreq=true} }, + resolvers.auto_equip_filters{MAINHAND = {type="weapon", subtype="greatmaul"}, }, + resolvers.equip{ {type="weapon", subtype="greatmaul", forbid_power_source={antimagic=true}, autoreq=true} }, autolevel = "caster", resists = { [DamageType.FIRE] = 100 }, diff --git a/game/modules/tome/data/general/npcs/naga.lua b/game/modules/tome/data/general/npcs/naga.lua index f31a45ae78c0798037aabf47f2d6424e349e51b0..4b549dcf4d275a4fa00cdddd5d53fc0a4eaa29cb 100644 --- a/game/modules/tome/data/general/npcs/naga.lua +++ b/game/modules/tome/data/general/npcs/naga.lua @@ -55,6 +55,7 @@ newEntity{ base = "BASE_NPC_NAGA", level_range = {30, nil}, exp_worth = 1, rarity = 1, max_life = resolvers.rngavg(120,150), life_rating = 16, + resolvers.auto_equip_filters{MAINHAND = {subtype="trident"},}, resolvers.equip{ {type="weapon", subtype="trident", autoreq=true, force_drop=true, special_rarity="trident_rarity"}, }, @@ -77,6 +78,9 @@ newEntity{ base = "BASE_NPC_NAGA", rank = 3, female = true, max_life = resolvers.rngavg(110,130), life_rating = 14, + resolvers.auto_equip_filters{MAINHAND = {subtype="longbow"}, + QUIVER = {subtype="arrow"}, + }, resolvers.equip{ {type="weapon", subtype="longbow", autoreq=true}, {type="ammo", subtype="arrow", autoreq=true}, diff --git a/game/modules/tome/data/general/npcs/orc-gorbat.lua b/game/modules/tome/data/general/npcs/orc-gorbat.lua index 2966c34d3a5a6fb37cfb7b17190603012ff1e554..c1e2f2ed14640b0b80da27eef8e7753e89a11134 100644 --- a/game/modules/tome/data/general/npcs/orc-gorbat.lua +++ b/game/modules/tome/data/general/npcs/orc-gorbat.lua @@ -50,6 +50,15 @@ newEntity{ ingredient_on_death = "ORC_HEART", } +local summoner_equip_filters = resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="sling"}, + QUIVER={properties={"archery_ammo"}, special=function(e, filter) -- must match the MAINHAND weapon, if any + local mh = filter._equipping_entity and filter._equipping_entity:getInven(filter._equipping_entity.INVEN_MAINHAND) + mh = mh and mh[1] + if not mh or mh.archery == e.archery_ammo then return true end + end} +} + newEntity{ base = "BASE_NPC_ORC_GORBAT", name = "orc summoner", color=colors.YELLOW, desc = [[A fierce orc attuned to the wilds.]], @@ -58,8 +67,10 @@ newEntity{ base = "BASE_NPC_ORC_GORBAT", rank = 2, max_life = resolvers.rngavg(80,110), life_rating = 12, + summoner_equip_filters, resolvers.equip{ - {type="weapon", subtype="sling", autoreq=true}, + {type="weapon", subtype="sling", forbid_power_source={arcane=true}, autoreq=true}, + {type="ammo", subtype="shot", forbid_power_source={arcane=true}, autoreq=true}, {type="charm", subtype="totem"} }, combat_armor = 2, combat_def = 0, @@ -86,8 +97,10 @@ newEntity{ base = "BASE_NPC_ORC_GORBAT", rank = 3, max_life = resolvers.rngavg(100,110), life_rating = 13, + summoner_equip_filters, resolvers.equip{ - {type="weapon", subtype="sling", autoreq=true}, + {type="weapon", subtype="sling", forbid_power_source={arcane=true}, autoreq=true}, + {type="ammo", subtype="shot", forbid_power_source={arcane=true}, autoreq=true}, {type="charm", subtype="totem"} }, combat_armor = 2, combat_def = 0, @@ -153,10 +166,11 @@ newEntity{ base = "BASE_NPC_ORC_GORBAT", rank = 3, max_life = resolvers.rngavg(120,150), life_rating = 15, + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ - {type="weapon", subtype="waraxe", autoreq=true}, - {type="armor", subtype="shield", autoreq=true}, - {type="armor", subtype="massive", autoreq=true}, + {type="weapon", subtype="waraxe", forbid_power_source={arcane=true}, autoreq=true}, + {type="armor", subtype="shield", forbid_power_source={arcane=true}, autoreq=true}, + {type="armor", subtype="massive", forbid_power_source={arcane=true}, autoreq=true}, }, combat_armor = 2, combat_def = 3, diff --git a/game/modules/tome/data/general/npcs/orc-grushnak.lua b/game/modules/tome/data/general/npcs/orc-grushnak.lua index 4e93fce8c5d7eeaa749cf3b3cb613f9e57f982c5..c0e2fb480866561e18ade2a90fc78f0e174401f4 100644 --- a/game/modules/tome/data/general/npcs/orc-grushnak.lua +++ b/game/modules/tome/data/general/npcs/orc-grushnak.lua @@ -55,6 +55,7 @@ newEntity{ base = "BASE_NPC_ORC_GRUSHNAK", level_range = {30, nil}, exp_worth = 1, rarity = 1, max_life = resolvers.rngavg(110,120), life_rating = 14, + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ {type="weapon", subtype="waraxe", autoreq=true}, {type="armor", subtype="shield", autoreq=true}, @@ -80,6 +81,7 @@ newEntity{ base = "BASE_NPC_ORC_GRUSHNAK", rarity = 3, rank = 3, max_life = resolvers.rngavg(170,180), life_rating = 14, + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ {type="weapon", subtype="waraxe", autoreq=true}, {type="armor", subtype="shield", autoreq=true}, @@ -110,6 +112,7 @@ newEntity{ base = "BASE_NPC_ORC_GRUSHNAK", level_range = {35, nil}, exp_worth = 1, rarity = 1, max_life = resolvers.rngavg(110,120), life_rating = 14, + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", autoreq=true}, {type="armor", subtype="massive", autoreq=true}, @@ -133,6 +136,7 @@ newEntity{ base = "BASE_NPC_ORC_GRUSHNAK", rarity = 3, rank = 3, max_life = resolvers.rngavg(170,180), life_rating = 14, + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", autoreq=true}, {type="armor", subtype="massive", autoreq=true}, diff --git a/game/modules/tome/data/general/npcs/orc-rak-shor.lua b/game/modules/tome/data/general/npcs/orc-rak-shor.lua index a577618d99d4c418531e9562d11063cef8fdcc86..2cb10adb9f95b586c24f2895ea3b1895014eb892 100644 --- a/game/modules/tome/data/general/npcs/orc-rak-shor.lua +++ b/game/modules/tome/data/general/npcs/orc-rak-shor.lua @@ -30,6 +30,7 @@ newEntity{ body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, QUIVER=1, TOOL=1 }, resolvers.drops{chance=20, nb=1, {} }, resolvers.drops{chance=10, nb=1, {type="money"} }, + resolvers.auto_equip_filters("Necromancer"), infravision = 10, lite = 1, diff --git a/game/modules/tome/data/general/npcs/orc-vor.lua b/game/modules/tome/data/general/npcs/orc-vor.lua index d231f1f050400a764d50c5a45d2e2636e86f50a8..dd9c28036b3116a2671aa57d4ab2d1cca45fa855 100644 --- a/game/modules/tome/data/general/npcs/orc-vor.lua +++ b/game/modules/tome/data/general/npcs/orc-vor.lua @@ -30,6 +30,7 @@ newEntity{ body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, QUIVER=1, TOOL=1 }, resolvers.drops{chance=20, nb=1, {} }, resolvers.drops{chance=10, nb=1, {type="money"} }, + resolvers.auto_equip_filters("Archmage"), infravision = 10, lite = 1, diff --git a/game/modules/tome/data/general/npcs/orc.lua b/game/modules/tome/data/general/npcs/orc.lua index 531b8b794cc6cd7602f8e81843a460d7df7f1141..642b2e6f1e349f9f954f8c1c759ee40f9df32740 100644 --- a/game/modules/tome/data/general/npcs/orc.lua +++ b/game/modules/tome/data/general/npcs/orc.lua @@ -53,6 +53,7 @@ newEntity{ base = "BASE_NPC_ORC", level_range = {10, nil}, exp_worth = 1, rarity = 1, max_life = resolvers.rngavg(70,80), + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ {type="weapon", subtype="waraxe", autoreq=true}, {type="armor", subtype="shield", autoreq=true}, @@ -82,6 +83,7 @@ newEntity{ base = "BASE_NPC_ORC", autolevel = "archer", resolvers.inscriptions(1, "infusion"), + resolvers.auto_equip_filters("Archer"), resolvers.equip{ {type="weapon", subtype="longbow", autoreq=true}, {type="ammo", subtype="arrow", autoreq=true}, @@ -176,6 +178,7 @@ newEntity{ base = "BASE_NPC_ORC", rarity = 3, infravision = 10, combat_armor = 2, combat_def = 12, + resolvers.auto_equip_filters("Rogue"), resolvers.equip{ {type="weapon", subtype="dagger", autoreq=true}, {type="weapon", subtype="dagger", autoreq=true}, @@ -205,6 +208,7 @@ newEntity{ base = "BASE_NPC_ORC", rank = 3, infravision = 10, combat_armor = 2, combat_def = 18, + resolvers.auto_equip_filters("Rogue"), resolvers.equip{ {type="weapon", subtype="dagger", ego_chance=20, autoreq=true}, {type="weapon", subtype="dagger", ego_chance=20, autoreq=true}, @@ -239,6 +243,7 @@ newEntity{ base = "BASE_NPC_ORC", rank = 3, infravision = 10, combat_armor = 2, combat_def = 18, + resolvers.auto_equip_filters("Rogue"), resolvers.equip{ {type="weapon", subtype="dagger", ego_chance=20, autoreq=true}, {type="weapon", subtype="dagger", ego_chance=20, autoreq=true}, @@ -279,7 +284,7 @@ newEntity{ base = "BASE_NPC_ORC", max_life = resolvers.rngavg(600, 800), life_rating = 22, move_others=true, - + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", defined="GAPING_MAW", random_art_replace={chance=75}, autoreq=true}, {type="armor", subtype="massive", tome_drops="boss", autoreq=true}, diff --git a/game/modules/tome/data/general/npcs/sunwall-town.lua b/game/modules/tome/data/general/npcs/sunwall-town.lua index 527cdaff1f97c875d51f661763cc07afcfec30e1..84ad7e94bb106f5c187564dbce8abc8a1a03dcb7 100644 --- a/game/modules/tome/data/general/npcs/sunwall-town.lua +++ b/game/modules/tome/data/general/npcs/sunwall-town.lua @@ -112,7 +112,7 @@ newEntity{ base = "BASE_NPC_SUNWALL_TOWN", max_life = resolvers.rngavg(70,80), resolvers.equip{ {type="weapon", subtype="staff", forbid_power_source={antimagic=true}, autoreq=true}, - {type="armor", subtype="robe", forbid_power_source={antimagic=true}, autoreq=true}, + {type="armor", subtype="cloth", forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.talents{ [Talents.T_STAFF_MASTERY]={base=1, every=10, max=5}, diff --git a/game/modules/tome/data/general/npcs/thieve.lua b/game/modules/tome/data/general/npcs/thieve.lua index b26b83512f207a4cdc3fda26d89b4fd1a35c3279..3d360b5f987144f6b63fdfddb2df16dad39b3490 100644 --- a/game/modules/tome/data/general/npcs/thieve.lua +++ b/game/modules/tome/data/general/npcs/thieve.lua @@ -17,8 +17,6 @@ -- Nicolas Casalini "DarkGod" -- darkgod@te4.org --- last updated: 9:25 AM 2/5/2010 - local Talents = require("engine.interface.ActorTalents") newEntity{ @@ -28,6 +26,7 @@ newEntity{ body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1 }, resolvers.drops{chance=20, nb=1, {} }, + resolvers.auto_equip_filters("Rogue"), resolvers.equip{ {type="weapon", subtype="dagger", autoreq=true}, {type="weapon", subtype="dagger", autoreq=true}, @@ -56,6 +55,7 @@ newEntity{ [Talents.T_KNIFE_MASTERY]={base=0, every=6, max=6}, [Talents.T_WEAPON_COMBAT]={base=0, every=6, max=6}, }, + power_source = {technique=true}, } diff --git a/game/modules/tome/data/general/npcs/yaech.lua b/game/modules/tome/data/general/npcs/yaech.lua index 67a5abcf924a93131406d42d56ab8b145bc79a11..bdd00763b9a196c00c54abd80352af98832dcd45 100644 --- a/game/modules/tome/data/general/npcs/yaech.lua +++ b/game/modules/tome/data/general/npcs/yaech.lua @@ -28,6 +28,7 @@ newEntity{ combat = { dam=resolvers.rngavg(5,12), atk=2, apr=6, physspeed=2 }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, QUIVER=1, TOOL=1 }, + resolvers.auto_equip_filters{MAINHAND = {subtype="trident"},}, resolvers.drops{chance=20, nb=1, {} }, resolvers.drops{chance=10, nb=1, {type="money"} }, infravision = 10, diff --git a/game/modules/tome/data/general/objects/world-artifacts-maj-eyal.lua b/game/modules/tome/data/general/objects/world-artifacts-maj-eyal.lua index b605e02a358d52603a144b31a96c5e911e802d53..a6677b630c48ad4c161f0eecde4e97b7c613a2fa 100644 --- a/game/modules/tome/data/general/objects/world-artifacts-maj-eyal.lua +++ b/game/modules/tome/data/general/objects/world-artifacts-maj-eyal.lua @@ -463,7 +463,7 @@ newEntity{ base = "BASE_SLING", wielder = { inc_stats = { [Stats.STAT_DEX] = 4, [Stats.STAT_CUN] = 3, }, inc_damage={ [DamageType.PHYSICAL] = 15 }, - talent_cd_reduction={[Talents.T_STEADY_SHOT]=1, [Talents.T_EYE_SHOT]=2}, + talent_cd_reduction={[Talents.T_STEADY_SHOT]=1, [Talents.T_SCATTER_SHOT]=2}, }, } diff --git a/game/modules/tome/data/general/objects/world-artifacts.lua b/game/modules/tome/data/general/objects/world-artifacts.lua index 979e2b29ed7e03ac97292c2ccc0aa05e94e14a4d..9c774c76d696ffaca333747cbed03475b4fd5b85 100644 --- a/game/modules/tome/data/general/objects/world-artifacts.lua +++ b/game/modules/tome/data/general/objects/world-artifacts.lua @@ -3547,6 +3547,7 @@ newEntity{ base = "BASE_GAUNTLETS", name = "Spellhunt Remnants", color = colors.GREY, image = "object/artifact/spellhunt_remnants.png", unided_name = "heavily corroded voratun gauntlets", desc = [[These once brilliant voratun gauntlets have fallen into a deep decay. Originally used in the spellhunt, they were often used to destroy arcane artifacts, curing the world of their influence.]], + special_desc = function(self) return "Drains arcane resources while worn." end, -- material_level = 1, --Special: this artifact can appear anywhere and adjusts its material level to the zone level_range = {1, nil}, rarity = 550, -- Extra rare to make it not ALWAYS appear. @@ -3558,10 +3559,25 @@ newEntity{ base = "BASE_GAUNTLETS", self.power_up(self, nil, mat_level) end end, + on_wear = function(self, who) + if who:attr("has_arcane_knowledge") then + game.logPlayer(who, "#ORCHID#The %s begin draining your arcane resources as they are worn!", self:getName({do_color=true})) + end + end, on_preaddobject = function(self, who, inven) -- generated in an actor's inventory if not self.material_level then self.addedToLevel(self, game.level) end end, cost = 1000, + callbackOnAct = function(self, who) -- Burn the wearer's arcane resources while worn + if who:attr("has_arcane_knowledge") then + local burn = who:burnArcaneResources(self.material_level*2) + if burn > 0 then + game.logSeen(who, "#ORCHID#%s's %s drain %s magic!", who.name:capitalize(), self:getName({do_color=true}), who:his_her()) + who:restStop("Antimagic Drain") + who:runStop("Antimagic Drain") + end + end + end, wielder = { combat_mindpower=4, combat_mindcrit=1, diff --git a/game/modules/tome/data/general/stores/basic.lua b/game/modules/tome/data/general/stores/basic.lua index 946e85400649cf3a391a546c6593dca06a796b08..4e0b974db3c3f9749d7cb413a6b11a9948dc88fa 100644 --- a/game/modules/tome/data/general/stores/basic.lua +++ b/game/modules/tome/data/general/stores/basic.lua @@ -58,7 +58,7 @@ newEntity{ empty_before_restock = false, filters = { {type="armor", subtype="cloth", id=true, tome_drops="store"}, - {type="armor", subtype="robe", id=true, tome_drops="store"}, +-- {type="armor", subtype="robe", id=true, tome_drops="store"}, {type="armor", subtype="cloak", id=true, tome_drops="store"}, {type="armor", subtype="belt", id=true, tome_drops="store"}, }, @@ -550,7 +550,7 @@ newEntity{ {type="armor", subtype="feet", id=true, tome_drops="boss"}, {type="armor", subtype="belt", id=true, tome_drops="boss"}, {type="armor", subtype="cloth", id=true, tome_drops="store"}, - {type="armor", subtype="robe", id=true, tome_drops="store"}, +-- {type="armor", subtype="robe", id=true, tome_drops="store"}, {type="armor", subtype="cloak", id=true, tome_drops="store"}, {type="armor", subtype="belt", id=true, tome_drops="store"}, {type="charm", subtype="torque", id=true, tome_drops="boss"}, diff --git a/game/modules/tome/data/general/traps/temporal.lua b/game/modules/tome/data/general/traps/temporal.lua index f554a54ad331fa07e082c71530437f88ec60c631..b0c5ea6f40048f78c476e0a2dd569cf2fa911611 100644 --- a/game/modules/tome/data/general/traps/temporal.lua +++ b/game/modules/tome/data/general/traps/temporal.lua @@ -44,7 +44,7 @@ newEntity{ base = "TRAP_TEMPORAL", name = "extremely disturbed pocket of time", auto_id = true, image = "trap/extremely_disturbed_pocket_of_time.png", detect_power = resolvers.clscale(8,25,8,0.5), disarm_power = resolvers.clscale(32,25,8,0.5), - rarity = 6, level_range = {1, nil}, + rarity = 6, level_range = {10, nil}, color=colors.PURPLE, message = "@Target@ is caught in an extremely distorted pocket of time!", unided_name = "distortion", diff --git a/game/modules/tome/data/maps/vaults/auto/greater/portal-vault.lua b/game/modules/tome/data/maps/vaults/auto/greater/portal-vault.lua index 3282c5bacd1d47a887cf9137dccba233087610d0..bb0e0a98fa6a73863be08d5e440ae6539f5576ae 100644 --- a/game/modules/tome/data/maps/vaults/auto/greater/portal-vault.lua +++ b/game/modules/tome/data/maps/vaults/auto/greater/portal-vault.lua @@ -80,11 +80,6 @@ end local RoomsLoader = require("engine.generator.map.RoomsLoader") -local locations = {} -- store teleport locations -local get_locations = function() - return locations -end - -- This uses the 'CHANGE_LEVEL' interface, but can be made to use another command -- NOT usable by npc's (would require some ai tweaks) local trigger = function(self, who) @@ -103,19 +98,11 @@ local trigger = function(self, who) game.logPlayer(who, "#YELLOW#The Portal repels you briefly before becoming quiescent. The other side seems to be blocked.") else require("engine.ui.Dialog"):yesnoPopup("Malevolant Portal", "An ominous aura emanates from this portal. Are you sure you want to go through?", function(ret) if ret then - game.logPlayer(who, "#YELLOW#An overcome intense #LIGHT_BLUE#REPULSIVE FORCES#LAST# as you traverse the Portal.") + game.logPlayer(who, "#YELLOW#You overcome intense #LIGHT_BLUE#REPULSIVE FORCES#LAST# as you traverse the Portal.") game:playSoundNear(who, "talents/distortion") who:move(tx, ty, true) end end, "Teleport", "Cancel") end - -- else - -- if not tx then - -- game.logSeen(who, "#YELLOW#The Portal repels %s briefly as %s approaches it.", who.name:capitalize(), who:he_she()) - -- else - -- who:logCombat(nil, "#YELLOW#The Portal #LIGHT_BLUE#VIOLENTLY DISTORTS#LAST# before #source# emerges from it.") - -- game:playSoundNear(who, "talents/distortion") - -- who:move(tx, ty, true) - -- end end else game.logPlayer(who, "#YELLOW#Nothing happens when you use the Portal.") @@ -141,7 +128,6 @@ end TeleportIn = mod.class.Grid.new{ define_as = "VAULT_TELEPORTER_IN", __position_aware = true, - _no_upvalues_check = true, type = "floor", subtype = "floor", name = "Portal", image = "terrain/marble_floor.png", add_displays={mod.class.Grid.new{z=5, image = "terrain/demon_portal.png"}}, desc = "A strange portal to some place else.", @@ -150,9 +136,7 @@ TeleportIn = mod.class.Grid.new{ on_move = move_trigger, on_stand = stand_trigger, change_level_check = trigger, - get_locations = get_locations, block_move = function(self, x, y) -- makes Map:updateMap update coordinates - self._locations = self.get_locations() table.merge(self._locations, {outx=x, outy=y}) if self._locations.inx and self._locations.iny and self._locations.outx and self._locations.outy then self.block_move = nil end return false @@ -162,7 +146,6 @@ TeleportIn = mod.class.Grid.new{ TeleportOut = mod.class.Grid.new{ define_as = "VAULT_TELEPORTER_OUT", __position_aware = true, - _no_upvalues_check = true, type = "floor", subtype = "floor", name = "Portal", image = "terrain/marble_floor.png", add_displays={mod.class.Grid.new{z=5, image = "terrain/demon_portal4.png"}}, desc = "A portal out of this place.", @@ -171,15 +154,17 @@ TeleportOut = mod.class.Grid.new{ on_move = move_trigger, on_stand = stand_trigger, change_level_check = trigger, - get_locations = get_locations, block_move = function(self, x, y) -- makes Map:updateMap update coordinates - self._locations = self.get_locations() table.merge(self._locations, {inx=x, iny=y}) if self._locations.inx and self._locations.iny and self._locations.outx and self._locations.outy then self.block_move = nil end return false end, } +local locations = {} +TeleportIn._locations = locations +TeleportOut._locations = locations + -- linked portal room local portal_def = {name = "portal_room", map_data={startx=2, starty=2}, diff --git a/game/modules/tome/data/maps/wilderness/eyal.lua b/game/modules/tome/data/maps/wilderness/eyal.lua index d5c7960621a29e29d6fcab72004d1d4268852a1c..89d4302a4b314f824155578128021811210a084e 100644 --- a/game/modules/tome/data/maps/wilderness/eyal.lua +++ b/game/modules/tome/data/maps/wilderness/eyal.lua @@ -440,6 +440,7 @@ addSpot({35, 35}, "world-encounter", "noxious-caldera") addSpot({53, 5}, "world-encounter", "sludgenest") addSpot({162, 59}, "world-encounter", "orc-breeding-pits-spawn") addSpot({47, 34}, "world-encounter", "conclave-vault") +addSpot({14, 25}, "world-encounter", "angolwen-quest") -- addZone section addZone({1, 1, 78, 43}, "zonename", "Maj'Eyal") diff --git a/game/modules/tome/data/quests/paradoxology.lua b/game/modules/tome/data/quests/paradoxology.lua index 82e81203430cbf0cd8d4cc7d0890c38a69f270bc..465cd9869f29b4d24737855fa8612c7adce862d5 100644 --- a/game/modules/tome/data/quests/paradoxology.lua +++ b/game/modules/tome/data/quests/paradoxology.lua @@ -36,25 +36,22 @@ on_status_change = function(self, who, status, sub) end generate = function(self, player, x, y) + local Talents = require("engine.interface.ActorTalents") local a = mod.class.NPC.new{} - a:replaceWith(player:resolveSource():cloneFull()) + local plr = player:resolveSource() + a:replaceWith(plr:cloneActor({rank=4, + level_range=self.level_range, + faction = "enemies", + life=plr.max_life*2, max_life=plr.max_life*2, max_level=table.NIL_MERGE, + name = plr.name.." the Paradox Mage", + desc = ([[A later (less fortunate?) version of %s, possibly going mad.]]):format(plr.name), + killer_message = "but nobody knew why #sex# suddenly became evil", + color_r=250, color_g=50, color_b=250, + ai = "tactical", ai_state = {talent_in=1}, + })) mod.class.NPC.castAs(a) engine.interface.ActorAI.init(a, a) - a.no_drops = true - a.keep_inven_on_death = false - a.energy.value = 0 - a.player = nil - a.rank = 4 - a.name = a.name.." the Paradox Mage" - a.color_r = 250 a.color_g = 50 a.color_b = 250 - a:removeAllMOs() - a.ai = "tactical" - a.ai_state = {talent_in=1} - a.faction = "enemies" - a.max_life = a.max_life * 2 - a.puuid = nil - a.life = a.max_life - + -- Remove all talents local tids = {} for tid, _ in pairs(a.talents) do @@ -62,19 +59,19 @@ generate = function(self, player, x, y) tids[#tids+1] = t end for i, t in ipairs(tids) do - if t.mode == "sustained" and a:isTalentActive(t.id) then a:forceUseTalent(t.id, {ignore_energy=true}) end - a.talents[t.id] = nil + a:unlearnTalentFull(t.id) end - -- Add talents - a:learnTalent(a.T_TEMPORAL_BOLT, true, 3) - a:learnTalent(a.T_TIME_SKIP, true, 3) - a:learnTalent(a.T_INDUCE_ANOMALY, true, 3) - a:learnTalent(a.T_REALITY_SMEARING, true, 3) - a:learnTalent(a.T_ASHES_TO_ASHES, true, 4) - a:learnTalent(a.T_DUST_TO_DUST, true, 4) - a:learnTalent(a.T_SEVER_LIFELINE, true, 5) - + -- And replace them with some paradox talents + table.insert(a, resolvers.talents{ + [Talents.T_TEMPORAL_BOLT]={base=1, every=8, max=5}, + [Talents.T_TIME_SKIP]={base=1, every=8, max=5}, + [Talents.T_INDUCE_ANOMALY]={base=1, every=8, max=5}, + [Talents.T_REALITY_SMEARING]={base=1, every=8, max=5}, + [Talents.T_ASHES_TO_ASHES]={base=1, every=7, max=6}, + [Talents.T_DUST_TO_DUST]={base=1, every=7, max=6}, + [Talents.T_SEVER_LIFELINE]={base=1, every=6, max=7}, + }) a.talent_cd_reduction = a.talent_cd_reduction or {} a.talents_cd[a.T_SEVER_LIFELINE] = 20 @@ -84,12 +81,12 @@ generate = function(self, player, x, y) a:incIncStat("wil", 200) a.anomaly_bias = {type = "temporal", chance=100} - a.self_resurrect = nil -- In case this is a skeleton player a.on_die = function(self) local o = game.zone:makeEntityByName(game.level, "object", "RUNE_RIFT") - o:identify(true) - game.zone:addEntity(game.level, o, "object", self.x, self.y) - + if o then + o:identify(true) + game.zone:addEntity(game.level, o, "object", self.x, self.y) + end game.player:setQuestStatus("paradoxology", engine.Quest.COMPLETED, "future-died") world:gainAchievement("PARADOX_FUTURE", p) game.logSeen(self, "#LIGHT_BLUE#Killing your own future self does feel weird, but you know that you can avoid this future. Just do not time travel.") @@ -126,8 +123,8 @@ generate = function(self, player, x, y) return true end end - game.zone:addEntity(game.level, a, "actor", x, y) + a:resolve() local chat = require("engine.Chat").new("paradoxology", a, player) chat:invoke() diff --git a/game/modules/tome/data/talents/celestial/celestial.lua b/game/modules/tome/data/talents/celestial/celestial.lua index 88e4bf71c56bde2460ef22b38d8c19ef10916e98..61c2d05c2381a37cbaf72379b967f74183e41e95 100644 --- a/game/modules/tome/data/talents/celestial/celestial.lua +++ b/game/modules/tome/data/talents/celestial/celestial.lua @@ -20,7 +20,7 @@ -- Corruptions newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/guardian", name = "guardian", min_lev = 10, description = "Your devotion grants you additional protection." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/chants", name = "chants", generic = true, description = "Chant the glory of the Sun." } -newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/chants-chants", name = "chants", generic = true, description = "Chant the glory of the Sun." } +newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/chants-chants", name = "chants", generic = true, on_mastery_change = function(self, m, tt) if self:knowTalentType("celestial/chants") ~= nil then self.talents_types_mastery[tt] = self.talents_types_mastery["celestial/chants"] end end, description = "Chant the glory of the Sun." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/light", name = "light", generic = true, description = "Invoke the power of the light to heal and mend." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/combat", name = "combat", description = "Your devotion allows you to combat your foes with indomitable determination." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/radiance", name = "radiance", description = "You channel the light of the sun through your body." } @@ -30,7 +30,7 @@ newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestia newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/glyphs", name = "glyphs", min_lev = 10, description = "Bind the brilliant powers into glyphs to trap your foes." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/twilight", name = "twilight", description = "Stand between the darkness and the light, harnessing both." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/star-fury", name = "star fury", description = "Call the fury of the Stars and the Moon to destroy your foes." } -newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/hymns", name = "hymns", generic = true, description = "Chant the glory of the Moon." } +newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/hymns", name = "hymns", generic = true, on_mastery_change = function(self, m, tt) if self:knowTalentType("celestial/hymns") ~= nil then self.talents_types_mastery[tt] = self.talents_types_mastery["celestial/hymns"] end end, description = "Chant the glory of the Moon." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/hymns-hymns", name = "hymns", generic = true, description = "Chant the glory of the Moon." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/circles", name = "circles", min_lev = 10, description = "Bind the power of the Moon into circles at your feet." } newTalentType{ allow_random=true, no_silence=true, is_spell=true, type="celestial/eclipse", name = "eclipse", description = "The moment of the Eclipse is the moment of Truth, when Sun and Moon are in tandem and the energies of the world hang in the balance. Intense focus allows the greatest Anorithils to harness these energies to unleash devastating forces..." } diff --git a/game/modules/tome/data/talents/celestial/circles.lua b/game/modules/tome/data/talents/celestial/circles.lua index 2647769562948ba86159baac6bd582d8f500bd30..610cd2138e62f499c518e2d03002c2594e512c83 100644 --- a/game/modules/tome/data/talents/celestial/circles.lua +++ b/game/modules/tome/data/talents/celestial/circles.lua @@ -185,7 +185,7 @@ newTalent{ local damage = t.getDamage(self, t) local duration = t.getDuration(self, t) local radius = self:getTalentRadius(t) - return ([[Creates a circle of radius %d at your feet; the circle slows incoming projectiles by %d%%, and attempts to push all creatures other then yourself out of its radius, inflicting %0.2f light damage and %0.2f darkness damage per turn as it does so. The circle lasts %d turns. + return ([[Creates a circle of radius %d at your feet; the circle slows incoming projectiles by %d%%, and attempts to push all creatures other than yourself out of its radius, inflicting %0.2f light damage and %0.2f darkness damage per turn as it does so. The circle lasts %d turns. The effects will increase with your Spellpower.]]): format(radius, damage*5, (damDesc (self, DamageType.LIGHT, damage)), (damDesc (self, DamageType.DARKNESS, damage)), duration) end, diff --git a/game/modules/tome/data/talents/celestial/combat.lua b/game/modules/tome/data/talents/celestial/combat.lua index 1371f2de6daa6b0493be6bb2e27b0f11f4318c42..7dc2df3e52408a1f880ccf45c156de8ba407187f 100644 --- a/game/modules/tome/data/talents/celestial/combat.lua +++ b/game/modules/tome/data/talents/celestial/combat.lua @@ -31,7 +31,7 @@ newTalent{ range = 10, getDamage = function(self, t) return 7 + self:combatSpellpower(0.092) * self:combatTalentScale(t, 1, 7) end, getShieldFlat = function(self, t) - return t.getDamage(self, t) / 2 + return t.getDamage(self, t) end, activate = function(self, t) game:playSoundNear(self, "talents/spell_generic2") @@ -45,7 +45,9 @@ newTalent{ return true end, callbackOnMeleeAttack = function(self, t, target, hitted, crit, weapon, damtype, mult, dam) + if self.turn_procs.weapon_of_light then return end if hitted and self:hasEffect(self.EFF_DAMAGE_SHIELD) and (self:reactionToward(target) < 0) then + self.turn_procs.weapon_of_light = true -- Shields can't usually merge, so change the parameters manually local shield = self:hasEffect(self.EFF_DAMAGE_SHIELD) local shield_power = t.getShieldFlat(self, t) @@ -56,6 +58,7 @@ newTalent{ shield.dur = math.max(2, shield.dur) -- Limit the number of times a shield can be extended, Bathe in Light also uses this code + -- The shield is removed instead of just blocking the extension to prevent scenarios such as maxing it out of combat then engaging, but there are likely better ways if shield.dur_extended then shield.dur_extended = shield.dur_extended + 1 if shield.dur_extended >= 20 then @@ -64,13 +67,12 @@ newTalent{ end else shield.dur_extended = 1 end end - end, info = function(self, t) local damage = t.getDamage(self, t) local shieldflat = t.getShieldFlat(self, t) return ([[Infuse your weapon with the power of the Sun, adding %0.1f light damage on each melee hit. - Additionally, if you have a temporary damage shield active, melee attacks will increase its power by %d. + Additionally, if you have a temporary damage shield active, melee hits will increase its power by %d once per turn. If the same shield is refreshed 20 times it will become unstable and explode, removing it. The damage dealt and shield bonus will increase with your Spellpower.]]): format(damDesc(self, DamageType.LIGHT, damage), shieldflat) @@ -176,7 +178,8 @@ newTalent{ local damagepct = t.getLifeDamage(self, t) local damage = t.getDamage(self, t) return ([[Your weapon attacks burn with righteous fury, dealing %d%% of your lost HP as additional Fire damage (up to %d, Current: %d). - Targets struck are also afflicted with a Martyrdom effect that causes them to take %d%% of all damage they deal for 4 turns.]]): + Targets struck are also afflicted with a Martyrdom effect that causes them to take %d%% of all damage they deal for 4 turns. + The bonus damage can only occur once per turn.]]): format(damagepct*100, t.getMaxDamage(self, t, 10, 400), damage, martyr) end, } diff --git a/game/modules/tome/data/talents/celestial/crusader.lua b/game/modules/tome/data/talents/celestial/crusader.lua index 80823d242651752a39eb517ca036b07edbb1e62f..c3450289314a755f13a27b915c80acb0ea15df80 100644 --- a/game/modules/tome/data/talents/celestial/crusader.lua +++ b/game/modules/tome/data/talents/celestial/crusader.lua @@ -81,12 +81,12 @@ newTalent{ local x, y, target = self:getTarget(tg) if not x or not y or not target then return nil end if core.fov.distance(self.x, self.y, x, y) > 5 then return nil end - target:setEffect(target.EFF_MARK_OF_LIGHT, 5, {src=self, power=t.getPower(self, t)}) + target:setEffect(target.EFF_MARK_OF_LIGHT, 3, {src=self, power=t.getPower(self, t)}) return true end, info = function(self, t) - return ([[You mark a target with light for 5 turns, causing all melee hits you deal to it to heal you for %d%% of the damage done.]]): + return ([[You mark a target with light for 3 turns, causing all melee hits you deal to it to heal you for %d%% of the damage done.]]): format(t.getPower(self, t)) end, } @@ -135,7 +135,7 @@ newTalent{ require = divi_req_high4, random_ego = "attack", points = 5, - cooldown = 9, + cooldown = 12, positive = 15, tactical = { ATTACKAREA = {LIGHT = 2} }, range = 0, diff --git a/game/modules/tome/data/talents/celestial/radiance.lua b/game/modules/tome/data/talents/celestial/radiance.lua index d8e5fefcfb698d05599100ec643da612bdab668c..c8bf8af99fefca60ea215c61cb15fbbb0053b5a6 100644 --- a/game/modules/tome/data/talents/celestial/radiance.lua +++ b/game/modules/tome/data/talents/celestial/radiance.lua @@ -18,11 +18,7 @@ -- darkgod@te4.org function radianceRadius(self) - if self:hasEffect(self.EFF_RADIANCE_DIM) then - return 1 - else - return self:getTalentRadius(self:getTalentFromId(self.T_RADIANCE)) - end + return self:getTalentRadius(self:getTalentFromId(self.T_RADIANCE)) end newTalent{ @@ -31,7 +27,7 @@ newTalent{ mode = "passive", require = divi_req1, points = 5, - radius = function(self, t) return self:combatTalentScale(t, 3, 7) end, + radius = function(self, t) return self:combatTalentLimit(t, 14, 3, 10) end, getResist = function(self, t) return self:combatTalentLimit(t, 100, 25, 75) end, passives = function(self, t, p) self:talentTemporaryValue(p, "radiance_aura", radianceRadius(self)) @@ -57,20 +53,22 @@ newTalent{ callbackOnActBase = function(self, t) local radius = radianceRadius(self) local grids = core.fov.circle_grids(self.x, self.y, radius, true) + local ss = self:isTalentActive(self.T_SEARING_SIGHT) + local ss_talent = self:getTalentFromId(self.T_SEARING_SIGHT) + local damage = ss_talent.getDamage(self, ss_talent) + local daze = ss_talent.getDaze(self, ss_talent) + for x, yy in pairs(grids) do for y, _ in pairs(grids[x]) do local target = game.level.map(x, y, Map.ACTOR) if target and self ~= target then if (self:reactionToward(target) < 0) then target:setEffect(target.EFF_ILLUMINATION, 1, {power=t.getPower(self, t), def=t.getDef(self, t)}) - local ss = self:isTalentActive(self.T_SEARING_SIGHT) - if ss then - local dist = core.fov.distance(self.x, self.y, target.x, target.y) - 1 - local coeff = math.max(0.1, 1 - (0.1*dist)) -- 10% less damage per distance - DamageType:get(DamageType.LIGHT).projector(self, target.x, target.y, DamageType.LIGHT, ss.dam * coeff) - if ss.daze and rng.percent(ss.daze) and target:canBe("stun") then - target:setEffect(target.EFF_DAZED, 3, {apply_power=self:combatSpellpower()}) + if ss and not target:hasEffect(target.EFF_DAZED) then + DamageType:get(DamageType.LIGHT).projector(self, target.x, target.y, DamageType.LIGHT, damage) + if daze and rng.percent(ss.daze) and target:canBe("stun") then + target:setEffect(target.EFF_DAZED, 5, {apply_power=self:combatSpellpower()}) end end end - end end end + end end end end, info = function(self, t) return ([[The light of your Radiance allows you to see that which would normally be unseen. @@ -81,8 +79,6 @@ newTalent{ end, } --- This doesn't work well in practice.. Its powerful but it leads to cheesy gameplay, spams combat logs, maybe even lags --- It can stay like this for now but may be worth making better newTalent{ name = "Searing Sight", type = {"celestial/radiance",3}, @@ -94,7 +90,7 @@ newTalent{ tactical = { ATTACKAREA = {LIGHT=1} }, sustain_positive = 10, getDamage = function(self, t) return self:combatTalentSpellDamage(t, 1, 35) end, - getDaze = function(self, t) return self:combatTalentLimit(t, 35, 5, 20) end, + getDaze = function(self, t) return self:combatTalentLimit(t, 35, 5, 25) end, updateParticle = function(self, t) local p = self:isTalentActive(self.T_SEARING_SIGHT) if not p then return end @@ -102,12 +98,9 @@ newTalent{ p.particle = self:addParticles(Particles.new("circle", 1, {toback=true, oversize=1, a=20, appear=4, speed=-0.2, img="radiance_circle", radius=self:getTalentRange(t)})) end, activate = function(self, t) - local daze = nil - if self:getTalentLevel(t) >= 4 then daze = t.getDaze(self, t) end return { particle = self:addParticles(Particles.new("circle", 1, {toback=true, oversize=1, a=20, appear=4, speed=-0.2, img="radiance_circle", radius=self:getTalentRange(t)})), - dam=t.getDamage(self, t), - daze=daze, + daze=t.getDaze(self, t), } end, deactivate = function(self, t, p) @@ -115,8 +108,8 @@ newTalent{ return true end, info = function(self, t) - return ([[Your Radiance is so powerful it burns all foes caught in it, doing up to %0.1f light damage (reduced with distance) to all foes caught inside. - At level 4 the light is so bright it has %d%% chance to daze them for 3 turns. + return ([[Your Radiance is so powerful it burns all foes caught in it, doing %0.1f light damage to all non-dazed foes caught inside. + Each enemy effected has a %d%% chance of being dazed for 5 turns. The damage increases with your Spellpower.]]): format(damDesc(self, DamageType.LIGHT, t.getDamage(self, t)), t.getDaze(self, t)) end, @@ -163,14 +156,11 @@ newTalent{ ) game.zone:addEntity(game.level, proj, "projectile", self.x, self.y) end) - - -- EFF_RADIANCE_DIM does nothing by itself its just used by radianceRadius - self:setEffect(self.EFF_RADIANCE_DIM, 5, {}) return true end, info = function(self, t) - return ([[Fire a glowing orb of light at each enemy within your Radiance. Each orb will slowly follow its target until it connects dealing %d light damage to anything else it contacts along the way. When the target is reached the orb will explode dealing %d light damage and healing you for 50%% of the damage dealt. This powerful ability will dim your Radiance, reducing its radius to 1 for 5 turns.]]): + return ([[Fire a glowing orb of light at each enemy within your Radiance. Each orb will slowly follow its target until it connects dealing %d light damage to anything else it contacts along the way. When the target is reached the orb will explode dealing %d light damage in radius 1 and healing you for 50%% of the damage dealt.]]): format(t.getMoveDamage(self, t), t.getExplosionDamage(self, t)) end, } diff --git a/game/modules/tome/data/talents/celestial/twilight.lua b/game/modules/tome/data/talents/celestial/twilight.lua index bf10f86efbe794e288fab64f8b20b15e42895436..b7bbd4a5ab4e0ca5fda4f74e3a07bdf09d4f8352 100644 --- a/game/modules/tome/data/talents/celestial/twilight.lua +++ b/game/modules/tome/data/talents/celestial/twilight.lua @@ -204,14 +204,16 @@ newTalent{ points = 5, cooldown = 30, negative = 10, - tactical = { DISABLE = 2 }, + tactical = { ATTACK = 2 }, requires_target = true, range = 5, - no_npc_use = true, +-- no_npc_use = true, + unlearn_on_clone = true, + target = function(self, t) return {type="bolt", range=self:getTalentRange(t), talent=t} end, getDuration = function(self, t) return math.floor(self:combatScale(self:getTalentLevel(t)+self:getCun(10), 3, 0, 18, 15)) end, - getPercent = function(self, t) return self:combatScale(self:getCun(10, true) * self:getTalentLevel(t), 0, 0, 50, 50) end, + getPercent = function(self, t) return self:combatLimit(self:getCun(10, true)*self:getTalentLevel(t), 90, 0, 0, 50, 50) end, action = function(self, t) - local tg = {type="bolt", range=self:getTalentRange(t), talent=t} + local tg = self:getTalentTarget(t) local tx, ty, target = self:getTarget(tg) if not tx or not ty then return nil end local _ _, tx, ty = self:canProject(tg, tx, ty) @@ -232,7 +234,7 @@ newTalent{ local allowed = 2 + math.ceil(self:getTalentLevelRaw(t) / 2 ) - if target.rank >= 3.5 or -- No boss + if target.rank >= 3.5 or -- No bosses target:reactionToward(self) >= 0 or -- No friends target.size_category > allowed then @@ -242,32 +244,22 @@ newTalent{ local modifier = t.getPercent(self, t) - local m = target:cloneFull{ + local m = target:cloneActor{ shader = "shadow_simulacrum", - no_drops = true, keep_inven_on_death = false, + name=target.name.."'s shadow simulacrum", faction = self.faction, summoner = self, summoner_gain_exp=true, summon_time = t.getDuration(self, t), + life=target.life*modifier/100, max_life=target.max_life*modifier/100, die_at=target.die_at*modifier/100, + exp_worth=0, forceLevelup=function() end, ai_target = {actor=target}, ai = "summoned", ai_real = target.ai, - resists = { all = modifier, [DamageType.DARKNESS] = 50, [DamageType.LIGHT] = - 50, }, - desc = [[A dark, shadowy shape whose form resembles the humanoid creature it was taken from. It is not a perfect replica, though, and it makes you feel uneasy to look at it.]], + desc = [[A dark, shadowy shape whose form resembles the creature it was copied from. It is not a perfect replica, though, and it makes you feel uneasy to look at it.]], } - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - - m.energy.value = 0 - m.life = m.life*modifier/100 - m.forceLevelup = function() end - -- Handle special things - m.on_die = nil - m.puuid = nil - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.exp_worth = 0 - m.clone_on_hit = nil + table.mergeAdd(m.resists, {all = modifier, [DamageType.DARKNESS]=50, [DamageType.LIGHT]=- 50}) + m:removeTimedEffectsOnClone() + m:unlearnTalentsOnClone() + if m.talents.T_SUMMON then m.talents.T_SUMMON = nil end if m.talents.T_MULTIPLY then m.talents.T_MULTIPLY = nil end @@ -289,7 +281,7 @@ newTalent{ size = "huge" end return ([[Creates a shadowy copy of a hostile target of up to %s size. The copy will attack its progenitor immediately and lasts for %d turns. - The duplicate has %d%% of the target's life, %d%% all damage resistance, +50%% darkness resistance and -50%% light resistance. + The duplicate has %d%% of the target's life, +%d%% all damage resistance, +50%% darkness resistance and -50%% light resistance. The duration, life and all damage resistance scale with your Cunning and this ability will not work on bosses.]]): format(size, duration, t.getPercent(self, t), t.getPercent(self, t)) end, diff --git a/game/modules/tome/data/talents/chronomancy/chronomancer.lua b/game/modules/tome/data/talents/chronomancy/chronomancer.lua index c7574675d46ca9c0868894df524f61d3b26993be..fa96de73943cdfdd4805486d76e9eb571a0c7933 100644 --- a/game/modules/tome/data/talents/chronomancy/chronomancer.lua +++ b/game/modules/tome/data/talents/chronomancy/chronomancer.lua @@ -293,25 +293,7 @@ makeParadoxClone = function(self, target, duration, alt_nodes) -- Don't copy certain fields from the target alt_nodes = alt_nodes or {} - if target:getInven("INVEN") then alt_nodes[target:getInven("INVEN")] = false end -- Skip main inventory; equipped items are still copied - alt_nodes.quests = false - alt_nodes.random_escort_levels = false - alt_nodes.achievements = false - alt_nodes.achievement_data = false - alt_nodes.last_learnt_talents = false - alt_nodes.died = false - alt_nodes.died_times = false - alt_nodes.killedBy = false - alt_nodes.all_kills = false - alt_nodes.all_kills_kind = false - alt_nodes.running_fov = false - alt_nodes.running_prev = false - alt_nodes._mo = false - alt_nodes._last_mo = false - alt_nodes.add_mos = false - alt_nodes.add_displays = false - alt_nodes.fov = {actors={}, actors_dist={}} - alt_nodes.distance_map = {} +-- if target:getInven("INVEN") then alt_nodes[target:getInven("INVEN")] = false end -- Skip main inventory; equipped items are still copied -- Don't copy some additional fields for short-lived clones if duration == 0 then @@ -321,74 +303,36 @@ makeParadoxClone = function(self, target, duration, alt_nodes) alt_nodes.talents_confirm_use = {} end - -- Clone the target - local m = target:cloneCustom(alt_nodes) + -- force some values in the clone + local clone_copy = {name=""..target.name.."'s temporal clone", + desc = [[A creature from another timeline.]], + faction=target.faction, exp_worth=0, + life=util.bound(target.life, target.die_at, target.max_life), + summoner=target, summoner_gain_exp=true, summon_time=duration, + max_level=target.level, + ai_target={actor=table.NIL_MERGE}, ai="summoned", + ai_real="tactical", ai_tactic={escape=0}, -- Clones never flee because they're awesome + } - -- Basic setup - m.no_drops = true - m.keep_inven_on_death = false - m.faction = target.faction - m.summoner = target - m.summoner_gain_exp = true - m.summon_time = duration - m.ai_target = {actor = nil} - m.ai = "summoned" - m.ai_real = "tactical" - m.name = "" .. target.name .. "'s temporal clone" - m.desc = [[A creature from another timeline.]] - - -- Remove some values - --m:removeAllMOs() - m.make_escort = nil - m.escort_quest = nil - m.on_added_to_level = nil - m.on_added = nil - m.game_ender = nil + -- Clone the target (Note: inventory access is disabled by default) + local m = target:cloneActor(clone_copy, alt_nodes) mod.class.NPC.castAs(m) engine.interface.ActorFOV.init(m) engine.interface.ActorAI.init(m, m) - -- Change some values - m.exp_worth = 0 - m.energy.value = 0 - m.player = nil - m.max_life = m.max_life - m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.on_die = nil - m.die = nil - m.puuid = nil - m.on_acquire_target = nil - m.no_inventory_access = true - m.no_levelup_access = true - m.on_takehit = nil - m.seen_by = nil - m.can_talk = nil - m.clone_on_hit = nil - m.unused_talents = 0 - m.unused_generics = 0 - m.unused_talents_types = 0 - m.unused_prodigies = 0 - if m.talents.T_SUMMON then m.talents.T_SUMMON = nil end - if m.talents.T_MULTIPLY then m.talents.T_MULTIPLY = nil end - - -- Clones never flee because they're awesome - m.ai_tactic = m.ai_tactic or {} - m.ai_tactic.escape = 0 - - -- Remove some talents + -- Remove some unallowed talents local tids = {} for tid, _ in pairs(m.talents) do local t = m:getTalentFromId(tid) - if (t.no_npc_use and not t.allow_temporal_clones) or t.remove_on_clone then tids[#tids+1] = t end + if (t.no_npc_use or t.unlearn_on_clone) and not t.allow_temporal_clones then tids[#tids+1] = t end end for i, t in ipairs(tids) do if t.mode == "sustained" and m:isTalentActive(t.id) then m:forceUseTalent(t.id, {ignore_energy=true, silent=true}) end m:unlearnTalentFull(t.id) end - -- Remove timed effects + -- Remove some timed effects m:removeTimedEffectsOnClone() -- Reset folds for Temporal Warden clones diff --git a/game/modules/tome/data/talents/chronomancy/guardian.lua b/game/modules/tome/data/talents/chronomancy/guardian.lua index 6c1ad779c427c5861a2542aa895b99e593557c1c..844d539a13d6746c8d27e6d1be7c496f05452f89 100644 --- a/game/modules/tome/data/talents/chronomancy/guardian.lua +++ b/game/modules/tome/data/talents/chronomancy/guardian.lua @@ -47,7 +47,7 @@ newTalent{ getDuration = function(self, t) return getExtensionModifier(self, t, 2) end, getLifeTrigger = function(self, t) return self:combatTalentLimit(t, 10, 30, 15) end, getDamageSplit = function(self, t) return self:combatTalentLimit(t, 40, 10, 30)/100 end, -- Limit < 40% - remove_on_clone = true, + unlearn_on_clone = true, callbackOnHit = function(self, t, cb, src) local split = cb.value * t.getDamageSplit(self, t) diff --git a/game/modules/tome/data/talents/chronomancy/other.lua b/game/modules/tome/data/talents/chronomancy/other.lua index 58064128801622ed39e5e03d76126d260b97d224..476228f7d6feb3896bbc92d2f223fd9ed5dbbc13 100644 --- a/game/modules/tome/data/talents/chronomancy/other.lua +++ b/game/modules/tome/data/talents/chronomancy/other.lua @@ -349,38 +349,24 @@ newTalent{ return end - local sex = game.player.female and "she" or "he" - local m = require("mod.class.NPC").new(self:cloneFull{ - no_drops = true, keep_inven_on_death = false, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - exp_worth = 0, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, - ai = "summoned", ai_real = "tactical", - ai_tactic = resolvers.tactic("ranged"), ai_state = { talent_in=1, ally_compassion=10}, - desc = [[The real you... or so ]]..sex..[[ says.]] - }) - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - - m.energy.value = 0 - m.player = nil - m.puuid = nil - m.max_life = m.max_life - m.life = util.bound(m.life, 0, m.max_life) + local m = makeParadoxClone(self, self, t.getDuration(self, t)) + -- Change some values + m.name = self.name.."'s Paradox Clone" + m.desc = ([[The real %s... or so %s says.]]):format(self.name, self:he_she()) + m.life = util.bound(m.life, m.die_at, m.max_life) m.forceLevelup = function() end - m.die = nil - m.on_die = nil - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.on_takehit = nil - m.no_inventory_access = true - m.clone_on_hit = nil - m.remove_from_party_on_death = true - + m.summoner = self + m.summoner_gain_exp = true + m.exp_worth = 0 + m.ai_target = {actor=nil} + m.ai = "summoned" + m.ai_real = "tactical" + -- Handle some AI stuff + m.ai_state = { talent_in=1, ally_compassion=10 } + ai_tactic = resolvers.tactic("ranged") + -- Try to use stored AI talents to preserve tweaking over multiple summons + m.ai_talents = self.stored_ai_talents and self.stored_ai_talents[m.name] or {} + -- Remove some talents local tids = {} for tid, _ in pairs(m.talents) do @@ -392,6 +378,7 @@ newTalent{ end game.zone:addEntity(game.level, m, "actor", x, y) + m:resolve() game.level.map:particleEmitter(x, y, 1, "temporal_teleport") game:playSoundNear(self, "talents/teleport") @@ -516,7 +503,7 @@ newTalent{ tactical = { ATTACK = 2, DISABLE = 2 }, requires_target = true, range = 10, - remove_on_clone = true, + unlearn_on_clone = true, target = function (self, t) return {type="hit", range=self:getTalentRange(t), talent=t, nowarning=true} end, @@ -550,7 +537,6 @@ newTalent{ m.generic_damage_penalty = t.getDamagePenalty(self, t) m.max_life = m.max_life * (100 - t.getDamagePenalty(self, t))/100 m.life = m.max_life - m.remove_from_party_on_death = true -- Handle some AI stuff m.ai_state = { talent_in=2, ally_compassion=10 } diff --git a/game/modules/tome/data/talents/chronomancy/temporal-hounds.lua b/game/modules/tome/data/talents/chronomancy/temporal-hounds.lua index f45e581814af3b406ca45387e0be6d88bf9ecc33..6e096a05d0bfbba97aae3e15b8dc191f76fecc99 100644 --- a/game/modules/tome/data/talents/chronomancy/temporal-hounds.lua +++ b/game/modules/tome/data/talents/chronomancy/temporal-hounds.lua @@ -152,6 +152,7 @@ newTalent{ points = 5, sustain_paradox = 48, no_sustain_autoreset = true, + unlearn_on_clone = true, cooldown = function(self, t) return math.ceil(self:combatTalentLimit(t, 10, 45, 15)) end, -- Limit >10 tactical = { BUFF = 2 }, callbackOnActBase = function(self, t) diff --git a/game/modules/tome/data/talents/chronomancy/threaded-combat.lua b/game/modules/tome/data/talents/chronomancy/threaded-combat.lua index 224639dd10a3f83d011e7df05722c27dbd2b9cc5..7610630fdd73225f4cf8d7583f8c10d44c45ae1a 100644 --- a/game/modules/tome/data/talents/chronomancy/threaded-combat.lua +++ b/game/modules/tome/data/talents/chronomancy/threaded-combat.lua @@ -268,7 +268,7 @@ newTalent{ require = chrono_req_high4, mode = "passive", points = 5, - remove_on_clone = true, + unlearn_on_clone = true, getChance = function(self, t) return self:combatTalentLimit(t, 65, 30, 50) end, --getDamagePenalty = function(self, t) return 100 - self:combatTalentLimit(t, 80, 10, 60) end, getDamagePenalty = function(self, t) return 100 - self:combatTalentLimit(t, 95, 60, 80) end, diff --git a/game/modules/tome/data/talents/chronomancy/timeline-threading.lua b/game/modules/tome/data/talents/chronomancy/timeline-threading.lua index bd7783a6dd45a82892885f28b3c2529ff87caba9..0bd2c54ddd6514dd57deaf05e59d9272001d227d 100644 --- a/game/modules/tome/data/talents/chronomancy/timeline-threading.lua +++ b/game/modules/tome/data/talents/chronomancy/timeline-threading.lua @@ -125,7 +125,7 @@ newTalent{ cooldown = 24, paradox = function(self, t) return getParadoxCost(self, t, 24) end, tactical = { ATTACK = 2, DISABLE = 2 }, - remove_on_clone = true, + unlearn_on_clone = true, getDuration = function(self, t) return getExtensionModifier(self, t, math.floor(self:combatTalentScale(t, 3, 8))) end, on_pre_use = function(self, t, silent) if self:hasEffect(self.EFF_TEMPORAL_FUGUE) then return false end return true end, action = function(self, t) @@ -133,16 +133,10 @@ newTalent{ -- Clone the caster local function makeFugueClone(self, t) - local sex = game.player.female and "she" or "he" local m = makeParadoxClone(self, self, t.getDuration(self, t)) -- Add and change some values - m.name = self.name - m.desc = [[The real you... or so ]]..sex..[[ says.]] - m.shader = nil - m.shader_args = nil - m.faction = self.faction - m.summoner = self - m.remove_from_party_on_death = true + m.name = self.name.."'s Fugue Clone" + m.desc = ([[The real %s... or so %s says.]]):format(self.name, self:he_she()) -- Handle some AI stuff m.ai_state = { talent_in=1, ally_compassion=10 } diff --git a/game/modules/tome/data/talents/corruptions/rot.lua b/game/modules/tome/data/talents/corruptions/rot.lua index 6b54fd2e1d358df3de4f1f94f83654e9c83750b1..8f3095a51bb8b626fc3b3fb307e4a50c8f02b19e 100644 --- a/game/modules/tome/data/talents/corruptions/rot.lua +++ b/game/modules/tome/data/talents/corruptions/rot.lua @@ -197,16 +197,9 @@ newTalent{ if not self.turn_procs.infestation then self.turn_procs.infestation = true - local nb = 0 - - local grids = {} - self:project({type="ball", range=0, radius=2, talent=t}, self.x, self.y, function(px, py) - if not ((px == x and py == y) or game.level.map:checkEntity(px, py, Map.TERRAIN, "block_move") or game.level.map(px, py, Map.TRAP)) then grids[#grids+1] = {x=px, y=py} end - end) - - local g = rng.tableRemove(grids) - if g then - carrionworm(self, self, 5, g.x, g.y) + local gx, gy = util.findFreeGrid(self.x, self.y, 2, true, {[Map.ACTOR]=true}) + if gx and gy then + carrionworm(self, self, 5, gx, gy) end end return cb.value diff --git a/game/modules/tome/data/talents/cunning/ambush.lua b/game/modules/tome/data/talents/cunning/ambush.lua index 6294e9bc42c1e52cde3565ba78bb82dc752fe868..b9f1301cd0d7cdd3af39745cd433ccbc3ec11de6 100644 --- a/game/modules/tome/data/talents/cunning/ambush.lua +++ b/game/modules/tome/data/talents/cunning/ambush.lua @@ -111,6 +111,7 @@ newTalent{ mana = 35, require = cuns_req_high3, requires_target = true, + unlearn_on_clone = true, tactical = { ATTACK = {DARKNESS = 3} }, getStealthPower = function(self, t) return self:combatScale(self:getCun(15, true) * self:getTalentLevel(t), 25, 0, 100, 75) end, getDuration = function(self, t) return math.floor(self:combatTalentScale(t, 4, 8)) end, @@ -125,52 +126,25 @@ newTalent{ return end - local m = self:cloneFull{ - shader = "shadow_simulacrum", - no_drops = true, keep_inven_on_death = false, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, - ai = "summoned", ai_real = "tactical", - name = "Shadow of "..self.name, - desc = [[A dark shadowy shape whose form resembles your own.]], - } - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - - m.energy.value = 0 - m.player = nil + local m = self:cloneActor({name = "Shadow of "..self.name, + desc = ([[A dark shadowy form in the shape of %s.]]):format(self.name), + summoner=self, summoner_gain_exp=true, exp_worth=0, + summon_time=t.getDuration(self, t), + ai_target={actor=nil}, ai="summoned", ai_real="tactical", + forceLevelup = function() end, + on_die = function(self) self:removeEffect(self.EFF_ARCANE_EYE,true) end, + cant_teleport=true, stealth = t.getStealthPower(self, t), + force_melee_damage_type = DamageType.DARKNESS, + + }) + m:removeTimedEffectsOnClone() + m:unlearnTalentsOnClone() -- unlearn certain talents (no recursive projections) + m:unlearnTalentFull(m.T_STEALTH) + m:unlearnTalentFull(m.T_HIDE_IN_PLAIN_SIGHT) m.max_life = m.max_life * t.getHealth(self, t) - m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.die = nil - m.on_die = function(self) self:removeEffect(self.EFF_ARCANE_EYE,true) end - m.on_acquire_target = nil - m.seen_by = nil - m.puuid = nil - m.on_takehit = nil - m.can_talk = nil - m.clone_on_hit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.no_levelup_access = true - m.cant_teleport = true - m:unlearnTalent(m.T_AMBUSCADE,m:getTalentLevelRaw(m.T_AMBUSCADE)) - m:unlearnTalent(m.T_PROJECTION,m:getTalentLevelRaw(m.T_PROJECTION)) -- no recurssive projections - m:unlearnTalent(m.T_STEALTH,m:getTalentLevelRaw(m.T_STEALTH)) - m:unlearnTalent(m.T_HIDE_IN_PLAIN_SIGHT,m:getTalentLevelRaw(m.T_HIDE_IN_PLAIN_SIGHT)) - m.stealth = t.getStealthPower(self, t) - - self:removeEffect(self.EFF_SHADOW_VEIL) -- Remove shadow veil from creator - m.remove_from_party_on_death = true - m.resists[DamageType.LIGHT] = -100 - m.resists[DamageType.DARKNESS] = 130 - m.resists.all = -30 + table.mergeAdd(m.resists, {[DamageType.LIGHT]=-70, [DamageType.DARKNESS]=130, all=-30}) m.inc_damage.all = ((100 + (m.inc_damage.all or 0)) * t.getDam(self, t)) - 100 - m.force_melee_damage_type = DamageType.DARKNESS - + m.life = util.bound(m.life, 0, m.max_life) m.on_act = function(self) if self.summoner.dead or not self:hasLOS(self.summoner.x, self.summoner.y) then if not self:hasEffect(self.EFF_AMBUSCADE_OFS) then @@ -183,6 +157,7 @@ newTalent{ end end, + self:removeEffect(self.EFF_SHADOW_VEIL) -- Remove shadow veil from creator game.zone:addEntity(game.level, m, "actor", x, y) game.level.map:particleEmitter(x, y, 1, "shadow") @@ -210,7 +185,7 @@ newTalent{ end, info = function(self, t) return ([[You take full control of your own shadow for %d turns. - Your shadow possesses your talents and stats, has %d%% life and deals %d%% damage, -30%% all resistances, -100%% light resistance and 100%% darkness resistance. + Your shadow possesses your talents and stats, has %d%% life and deals %d%% damage, -30%% all resistances, -100%% light resistance and +100%% darkness resistance. Your shadow is permanently stealthed (%d power), and all melee damage it deals is converted to darkness damage. The shadow cannot teleport. If you release control early or if it leaves your sight for too long, your shadow will dissipate.]]): diff --git a/game/modules/tome/data/talents/cursed/shadows.lua b/game/modules/tome/data/talents/cursed/shadows.lua index ced9cf1941becc28c7766708714eeab65a98cca1..ed4923d76fe19f52304d8cf09a3ee6158257ac3b 100644 --- a/game/modules/tome/data/talents/cursed/shadows.lua +++ b/game/modules/tome/data/talents/cursed/shadows.lua @@ -341,6 +341,7 @@ newTalent{ points = 5, cooldown = 10, hate = 0, + unlearn_on_clone = true, tactical = { BUFF = 5 }, getLevel = function(self, t) return self.level end, getMaxShadows = function(self, t) @@ -606,6 +607,9 @@ newTalent{ requires_target = true, tactical = { ATTACK = 2 }, target = function(self, t) return {type="hit", range=self:getTalentRange(t), nowarning=true} end, + on_pre_use = function(self, t, silent) + return self:isTalentActive(self.T_CALL_SHADOWS) + end, getDefenseDuration = function(self, t) return math.floor(self:combatTalentScale(t, 4.4, 10.1)) end, getBlindsideChance = function(self, t) return self:combatTalentLimit(t, 100, 40, 80) end, -- Limit < 100% action = function(self, t) @@ -639,7 +643,7 @@ newTalent{ self:logCombat(target, "#PINK#The shadows converge on #Target#!") return true else - game.logPlayer(self, "Their are no shadows to heed the call!") + game.logPlayer(self, "There are no shadows to heed the call!") return false end else diff --git a/game/modules/tome/data/talents/gifts/dwarven-nature.lua b/game/modules/tome/data/talents/gifts/dwarven-nature.lua index 5a2919e63205e8e67dbea19a61ae25e2a2447288..5a4640c3db19d434ef007eed8a5e7339e7b9f581 100644 --- a/game/modules/tome/data/talents/gifts/dwarven-nature.lua +++ b/game/modules/tome/data/talents/gifts/dwarven-nature.lua @@ -82,50 +82,31 @@ newTalent{ if cryst or stone then return false end return true end, + unlearn_on_clone = true, action = function(self, t) local nb_halves = 0 -- Find space local x, y = util.findFreeGrid(self.x, self.y, 1, true, {[Map.ACTOR]=true}) if x then - local m = require("mod.class.NPC").new(self:cloneFull{ + local m = require("mod.class.NPC").new(self:cloneActor{ shader = "shadow_simulacrum", shader_args = {time_factor=1000, base=0.5, color={0.6, 0.3, 0.6}}, - no_drops = true, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, - ai = "summoned", ai_real = "tactical", - name = "Crystaline Half ("..self.name..")", - desc = [[A crystaline structure that has taken your exact form.]], + summoner=self, summoner_gain_exp=true, exp_worth=0, + max_level=self.level, + summon_time=t.getDuration(self, t), + ai_target={actor=nil}, + ai="summoned", ai_real="tactical", + name="Crystaline Half ("..self.name..")", + desc=([[A crystaline structure that has taken the form of %s.]]):format(self.name), }) - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - - m.energy.value = 0 - m.player = nil - -- m.max_life = m.max_life * t.getHealth(self, t) - -- m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.die = nil - m.on_die = nil - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.puuid = nil - m.on_takehit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.clone_on_hit = nil local tids = table.keys(m.talents) for i, tid in ipairs(tids) do if tid ~= m.T_STONESHIELD and tid ~= m.T_ARMOUR_TRAINING then - m:unlearnTalent(tid, m:getTalentLevelRaw(tid)) + m:unlearnTalentFull(tid) end end + m:learnTalent(m.T_DWARVEN_HALF_EARTHEN_MISSILES, true, self:getTalentLevelRaw(t)) - m.remove_from_party_on_death = true if self:knowTalent(self.T_POWER_CORE) then m:learnTalent(m.T_RAIN_OF_SPIKES, true, math.floor(self:getTalentLevel(self.T_POWER_CORE))) @@ -151,36 +132,16 @@ newTalent{ -- Find space local x, y = util.findFreeGrid(self.x, self.y, 1, true, {[Map.ACTOR]=true}) if x then - local m = require("mod.class.NPC").new(self:cloneFull{ + local m = require("mod.class.NPC").new(self:cloneActor{ shader = "shadow_simulacrum", shader_args = {time_factor=-8000, base=0.5, color={0.6, 0.4, 0.3}}, - no_drops = true, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, - ai = "summoned", ai_real = "tactical", - name = "Stone Half ("..self.name..")", - desc = [[A stone structure that has taken your exact form.]], + summoner=self, summoner_gain_exp=true, exp_worth=0, + max_level=self.level, + summon_time=t.getDuration(self, t), + ai_target={actor=nil}, + ai="summoned", ai_real="tactical", + name="Stone Half ("..self.name..")", + desc=([[A stone structure that has taken the form of %s.]]):format(self.name), }) - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - - m.energy.value = 0 - m.player = nil - -- m.max_life = m.max_life * t.getHealth(self, t) - -- m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.die = nil - m.on_die = nil - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.puuid = nil - m.on_takehit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.clone_on_hit = nil local tids = table.keys(m.talents) for i, tid in ipairs(tids) do if tid ~= m.T_STONESHIELD and tid ~= m.T_ARMOUR_TRAINING then @@ -189,8 +150,7 @@ newTalent{ end m.talent_cd_reduction={[m.T_TAUNT]=3}, m:learnTalent(m.T_TAUNT, true, self:getTalentLevelRaw(t)) - m.remove_from_party_on_death = true - + if self:knowTalent(self.T_POWER_CORE) then m:learnTalent(m.T_STONE_LINK, true, math.floor(self:getTalentLevel(self.T_POWER_CORE))) end diff --git a/game/modules/tome/data/talents/misc/npcs.lua b/game/modules/tome/data/talents/misc/npcs.lua index 4c3a91b561d281d6452b7b1225ac7d3f79c556ac..291bc605d6fb24b234dbe93cc6a34845b35588d6 100644 --- a/game/modules/tome/data/talents/misc/npcs.lua +++ b/game/modules/tome/data/talents/misc/npcs.lua @@ -40,36 +40,35 @@ newTalent{ cooldown = 3, range = 10, requires_target = true, + on_pre_use = function(self, t) return self.can_multiply and self.can_multiply > 0 end, + unlearn_on_clone = true, tactical = { ATTACK = function(self, t, aitarget) return 2*(1.2 + self.level/50) end }, action = function(self, t) + if not self.can_multiply or self.can_multiply <= 0 then game.logPlayer(self, "You can not multiply anymore.") return nil end - -- Find space + -- Find a place for the clone local x, y = util.findFreeGrid(self.x, self.y, 1, true, {[Map.ACTOR]=true}) - if not x then print("no free space") return nil end + if not x then print("Multiply: no free space") return nil end - -- Find a place around to clone self.can_multiply = self.can_multiply - 1 - local a - if self.clone_base then a = self.clone_base:cloneFull() else a = self:cloneFull() end - a.can_multiply = a.can_multiply - 1 - a.energy.value = 0 - a.exp_worth = 0.1 - a.inven = {} - a.x, a.y = nil, nil - a.faction = self.faction - a:removeAllMOs() + local a = self:cloneActor({can_multiply=self.can_multiply-1, exp_worth=0.1}) + mod.class.NPC.castAs(a) + a:removeTimedEffectsOnClone() - if a.can_multiply <= 0 then a:unlearnTalent(t.id) end + a:unlearnTalentsOnClone() + -- allow chain multiply for now (It's classic!) + if a.can_multiply > 0 then a:learnTalent(t.id, true, 1) end print("[MULTIPLY]", x, y, "::", game.level.map(x,y,Map.ACTOR)) print("[MULTIPLY]", a.can_multiply, "uids", self.uid,"=>",a.uid, "::", self.player, a.player) game.zone:addEntity(game.level, a, "actor", x, y) a:check("on_multiply", self) + a:doFOV() return true end, info = function(self, t) - return ([[Multiply yourself!]]) + return ([[Multiply yourself! (up to %d times)]]):format(self.can_multiply or 0) end, } @@ -362,6 +361,7 @@ newTalent{ requires_target = true, tactical = { ATTACK = 2 }, is_summon = true, + unlearn_on_clone = true, action = function(self, t) if not self:canBe("summon") then game.logPlayer(self, "You cannot summon; you are suppressed!") return end diff --git a/game/modules/tome/data/talents/psionic/finer-energy-manipulations.lua b/game/modules/tome/data/talents/psionic/finer-energy-manipulations.lua index 5473185493152e36483d29a59b083448b0a70eca..32d890a9cfae473f1c4346fd9ffa72a7b4da18d2 100644 --- a/game/modules/tome/data/talents/psionic/finer-energy-manipulations.lua +++ b/game/modules/tome/data/talents/psionic/finer-energy-manipulations.lua @@ -83,14 +83,13 @@ newTalent{ } newTalent{ - name = "Reshape Weapon/Armour", image = "talents/reshape_weapon.png", + name = "Form and Function", image = "talents/reshape_weapon.png", type = {"psionic/finer-energy-manipulations", 2}, require = psi_cun_req2, mode = "passive", points = 5, no_npc_use = true, - no_unlearn_last = true, - damBoost = function(self, t) return math.floor(self:combatTalentMindDamage(t, 5, 20)) end, + damBoost = function(self, t) return math.floor(self:combatTalentMindDamage(t, 5, 25)) end, armorBoost = function(self, t) return math.floor(self:combatTalentMindDamage(t, 5, 20)) end, fatigueBoost = function(self, t) return math.floor(self:combatTalentMindDamage(t, 2, 10)) end, getDamBoost = function(self, t, weapon) @@ -115,8 +114,8 @@ newTalent{ local weapon_boost = t.damBoost(self, t) local arm = t.armorBoost(self, t) local fat = t.fatigueBoost(self, t) - return ([[Manipulate forces on the molecular level to realign, rebalance, and hone a weapon, set of body armor, or a shield. (Mindstars resist being adjusted because they are already in an ideal natural state.) - The accuracy and damage of any weapon will act as if %d higher. + return ([[Manipulate forces on the molecular level to realign, rebalance, and synergize equipment you wear to your form and function. + Any weapon you wield will gain a boost of %d to both accuracy and power. (The power is treated like an increase to your stats. Mindstars cannot be manipulated in this way because they are already in an ideal natural state.) Your total armour will increase by %d and your fatigue rating by %d for each body armour and shield worn. The effects increase with your Mindpower.]]): format(weapon_boost, arm, fat) diff --git a/game/modules/tome/data/talents/psionic/mentalism.lua b/game/modules/tome/data/talents/psionic/mentalism.lua index e505902944d77682dbf7fe73725f586ea4687326..f1334064502f22629919d8c45ac406f34e95a204 100644 --- a/game/modules/tome/data/talents/psionic/mentalism.lua +++ b/game/modules/tome/data/talents/psionic/mentalism.lua @@ -121,6 +121,7 @@ newTalent{ psi = 20, cooldown = function(self, t) return math.ceil(self:combatTalentLimit(t, 0, 17.5, 9.5)) end, -- Limit >0 no_npc_use = true, -- this can be changed if the AI is improved. I don't trust it to be smart enough to leverage this effect. + unlearn_on_clone = true, getPower = function(self, t) return math.ceil(self:combatTalentMindDamage(t, 5, 40)) end, getDuration = function(self, t) return math.floor(self:combatTalentScale(t, 6, 14)) end, action = function(self, t) @@ -131,57 +132,29 @@ newTalent{ return end - local m = self:cloneFull{ - no_drops = true, keep_inven_on_death = false, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, + local m = self:cloneActor{ + summoner=self, summoner_gain_exp=true, summon_time = t.getDuration(self, t), exp_worth=0, + _rst_full=true, can_change_level=table.NIL_MERGE, can_change_zone=table.NIL_MERGE, + life = util.bound(self.life, self.die_at, self.max_life), + max_level=self.level, + ai_target={actor=table.NIL_MERGE}, ai = "summoned", ai_real = "tactical", subtype = "ghost", is_psychic_projection = 1, name = "Projection of "..self.name, desc = [[A ghostly figure.]], + lite=0, } - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - m._rst_full = true - - m.energy.value = 0 - m.player = nil - m.max_life = m.max_life - m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.die = nil - m.on_die = nil - m.on_acquire_target = nil - m.seen_by = nil - m.puuid = nil - m.on_takehit = nil - m.can_talk = nil - m.clone_on_hit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.can_change_level = false - m.remove_from_party_on_death = true - for i = 1, 10 do - m:unlearnTalent(m.T_AMBUSCADE) -- no recurssive projections - m:unlearnTalent(m.T_PROJECTION) - m:unlearnTalent(m.T_THOUGHT_FORMS) - end - - m.can_pass = {pass_wall=70} - m.no_breath = 1 - m.invisible = (m.invisible or 0) + t.getPower(self, t)/2 - m.see_invisible = (m.see_invisible or 0) + t.getPower(self, t) - m.see_stealth = (m.see_stealth or 0) + t.getPower(self, t) - m.lite = 0 - m.infravision = (m.infravision or 0) + 10 - m.avoid_pressure_traps = 1 - + m:removeTimedEffectsOnClone() + m:unlearnTalentsOnClone() -- unlearn certain talents (no recursive projections) + table.mergeAdd(m, {can_pass = {pass_wall=70}}, true) + m:attr("invisible", t.getPower(self, t)/2) + m:attr("see_invisible", t.getPower(self, t)/2) + m:attr("see_stealth", t.getPower(self, t)/2) + m:attr("lite", -10) + m:attr("no_breath", 1) + m:attr("infravision", 10) + m:attr("avoid_pressure_traps", 1) - -- Connection to the summoner functions - local summon_time = t.getDuration(self, t) --summoner takes hit m.on_takehit = function(self, value, src) self.summoner:takeHit(value, src) return value end --pass actors targeting us back to the summoner to prevent super cheese diff --git a/game/modules/tome/data/talents/psionic/nightmare.lua b/game/modules/tome/data/talents/psionic/nightmare.lua index 7e573fed2317838a554d9c5218f80f8c5ae97afd..a5d5be624296fa4c4590b86236a005c68f85a262 100644 --- a/game/modules/tome/data/talents/psionic/nightmare.lua +++ b/game/modules/tome/data/talents/psionic/nightmare.lua @@ -96,13 +96,14 @@ newTalent{ range = 7, direct_hit = true, requires_target = true, + unlearn_on_clone = true, tactical = { ATTACK = function(self, t, target) if target and target:attr("sleep") then return 4 else return 2 end end }, getChance = function(self, t, crit) local tl = self:combatTalentMindDamage(t, 15, 30) if crit then tl = self:mindCrit(tl) end return self:combatLimit(tl, 100, 0, 0, 21, 21) -- Limit < 100% end, - getDuration = function(self, t) return math.floor(self:combatTalentScale(t, 4, 12)) end, + getDuration = function(self, t) return self:combatTalentLimit(t, 15, 2, 12) end, summon_inner_demons = function(self, target, t) -- Find space local x, y = util.findFreeGrid(target.x, target.y, 1, true, {[Map.ACTOR]=true}) @@ -110,67 +111,32 @@ newTalent{ return end if target:attr("summon_time") then return end - - local m = target:cloneFull{ - shader = "shadow_simulacrum", - shader_args = { color = {0.6, 0.0, 0.3}, base = 0.6, time_factor = 1500 }, - no_drops = true, keep_inven_on_death = false, + local ml = target.max_life/2/target.rank + local m = target:cloneActor{ + shader = "shadow_simulacrum", shader_args = { color = {0.6, 0.0, 0.3}, base = 0.6, time_factor = 1500 }, faction = self.faction, - summoner = self, summoner_gain_exp=true, + summoner = self, summoner_gain_exp=true, exp_worth=0, summon_time = 10, + max_life = ml, life = util.bound(target.life, target.die_at, ml), + max_level = target.level, ai_target = {actor=target}, ai = "summoned", ai_real = "tactical", + ai_tactic={escape=0}, -- never flee name = ""..target.name.."'s Inner Demon", desc = [[A hideous, demonic entity that resembles the creature it came from.]], } - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - m.on_added = nil - mod.class.NPC.castAs(m) engine.interface.ActorAI.init(m, m) - - m.exp_worth = 0 - m.energy.value = 0 - m.player = nil - m.max_life = m.max_life / 2 / m.rank - m.life = util.bound(m.life, 0, m.max_life) m.inc_damage.all = (m.inc_damage.all or 0) - 50 - m.forceLevelup = function() end - m.on_die = nil - m.die = nil - m.puuid = nil - m.on_acquire_target = nil - m.no_inventory_access = true - m.on_takehit = nil - m.seen_by = nil - m.can_talk = nil - m.clone_on_hit = nil - m.self_resurrect = nil - if m.talents.T_SUMMON then m.talents.T_SUMMON = nil end - if m.talents.T_MULTIPLY then m.talents.T_MULTIPLY = nil end - - -- Inner Demon's never flee - m.ai_tactic = m.ai_tactic or {} - m.ai_tactic.escape = 0 -- Remove some talents - local tids = {} - for tid, _ in pairs(m.talents) do - local t = m:getTalentFromId(tid) - if t.no_npc_use then tids[#tids+1] = t end - end - for i, t in ipairs(tids) do - if t.mode == "sustained" and m:isTalentActive(t.id) then m:forceUseTalent(t.id, {ignore_energy=true}) end - m.talents[t.id] = nil - end - - -- remove detrimental timed effects + m:unlearnTalentsOnClone() + + -- remove detrimental and disallowed timed effects local effs = {} for eff_id, p in pairs(m.tmp) do local e = m.tempeffect_def[eff_id] - if e.status == "detrimental" then + if e.status == "detrimental" or e.remove_on_clone then effs[#effs+1] = {"effect", eff_id} end end @@ -201,10 +167,8 @@ newTalent{ return nil end - -- "INNER_DEMONS" effect in data\time_effects\mental.lua calculates sleeping target effect - local chance = t.getChance(self, t, true) - - local chance = self:mindCrit(t.getChance(self, t)) + -- "INNER_DEMONS" effect in data\time_effects\mental.lua calculates sleeping target effect + local chance = t.getChance(self, t, false) if target:canBe("fear") or target:attr("sleep") then target:setEffect(target.EFF_INNER_DEMONS, t.getDuration(self, t), {src = self, chance=chance, apply_power=self:combatMindpower()}) else @@ -219,7 +183,8 @@ newTalent{ local chance = t.getChance(self, t) return ([[Brings the target's inner demons to the surface. Each turn, for %d turns, there's a %d%% chance that a demon will surface, requiring the target to make a Mental Save to keep it from manifesting. If the target is sleeping, the chance to save will be halved, and fear immunity will be ignored. Otherwise, if the summoning is resisted, the effect will end early. - The summon chance will scale with your Mindpower and the demon's life will scale with the target's rank.]]):format(duration, chance) + The summon chance will scale with your Mindpower and the demon's life will scale with the target's rank. + If a demon manifests the sheer terror will remove all sleep effects from the victim, but not the Inner Demons.]]):format(duration, chance) end, } diff --git a/game/modules/tome/data/talents/psionic/thought-forms.lua b/game/modules/tome/data/talents/psionic/thought-forms.lua index 061b2f9a6eef58aa206e3ae3cb1ff3102e3819e7..387fe57310bebe6221c4705c4e0882f994a93360 100644 --- a/game/modules/tome/data/talents/psionic/thought-forms.lua +++ b/game/modules/tome/data/talents/psionic/thought-forms.lua @@ -476,6 +476,7 @@ newTalent{ require = psi_wil_req1, mode = "passive", range = 10, + unlearn_on_clone = true, getStatBonus = function(self, t) return self:combatTalentMindDamage(t, 5, 50) end, on_learn = function(self, t) if self:getTalentLevel(t) >= 1 and not self:knowTalent(self.T_TF_BOWMAN) then diff --git a/game/modules/tome/data/talents/spells/necrotic-minions.lua b/game/modules/tome/data/talents/spells/necrotic-minions.lua index f8eae424dd6be3ce6d870777344e9dd4b483502d..94c4fc67132e033e826911f05e1279ff0604d1cc 100644 --- a/game/modules/tome/data/talents/spells/necrotic-minions.lua +++ b/game/modules/tome/data/talents/spells/necrotic-minions.lua @@ -737,6 +737,7 @@ newTalent{ requires_target = true, range = 0, autolearn_talent = "T_NECROTIC_AURA", + unlearn_on_clone = true, radius = function(self, t) local aura = self:getTalentFromId(self.T_NECROTIC_AURA) return aura.getRadius(self, aura) diff --git a/game/modules/tome/data/talents/spells/shades.lua b/game/modules/tome/data/talents/spells/shades.lua index 55548f032e89e45587164e1a32ff13c020c5d9b7..387e47fbc3922bce354c9fe7182a13cdc3ef1e9c 100644 --- a/game/modules/tome/data/talents/spells/shades.lua +++ b/game/modules/tome/data/talents/spells/shades.lua @@ -177,6 +177,7 @@ newTalent{ range = 10, tactical = { ATTACK = 2, }, requires_target = true, + unlearn_on_clone = true, getDuration = function(self, t) return math.floor(self:combatTalentLimit(t, 30, 4, 8.1)) end, -- Limit <30 getHealth = function(self, t) return self:combatLimit(self:combatTalentSpellDamage(t, 20, 500), 1.0, 0.2, 0, 0.58, 384) end, -- Limit health < 100% getDam = function(self, t) return self:combatLimit(self:combatTalentSpellDamage(t, 10, 500), 1.40, 0.4, 0, 0.76, 361) end, -- Limit damage < 140% @@ -187,40 +188,24 @@ newTalent{ game.logPlayer(self, "Not enough space to summon!") return end - - local m = require("mod.class.NPC").new(self:cloneFull{ + local hfct = t.getHealth(self, t) + local m = require("mod.class.NPC").new(self:cloneActor{ shader = "shadow_simulacrum", - no_drops = true, - faction = self.faction, - summoner = self, summoner_gain_exp=true, - summon_time = t.getDuration(self, t), - ai_target = {actor=nil}, + faction = self.faction, exp_worth = 0, + max_life = self.max_life*hfct, die_at = self.die_at*hfct, + life = util.bound(self.life*hfct, self.die_at*hfct, self.max_life*hfct), + max_level = self.level, + summoner = self, summoner_gain_exp=true, summon_time = t.getDuration(self, t), + + ai_target = {actor=table.NIL_MERGE}, ai = "summoned", ai_real = "tactical", name = "Forgery of Haze ("..self.name..")", - desc = [[A dark shadowy shape whose form resembles yours.]], + desc = ([[A dark shadowy shape whose form resembles %s.]]):format(self.name), }) - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - m.energy.value = 0 - m.player = nil - m.max_life = m.max_life * t.getHealth(self, t) - m.life = util.bound(m.life, 0, m.max_life) - m.forceLevelup = function() end - m.die = nil - m.on_die = nil - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.puuid = nil - m.on_takehit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.clone_on_hit = nil - m:unlearnTalentFull(m.T_CREATE_MINIONS) - m:unlearnTalentFull(m.T_FORGERY_OF_HAZE) - m.remove_from_party_on_death = true + m:removeTimedEffectsOnClone() + m:unlearnTalentsOnClone() + m.inc_damage.all = ((100 + (m.inc_damage.all or 0)) * t.getDam(self, t)) - 100 game.zone:addEntity(game.level, m, "actor", x, y) diff --git a/game/modules/tome/data/talents/techniques/buckler-training.lua b/game/modules/tome/data/talents/techniques/buckler-training.lua index e9a835e13a34d39f8b8c86a25d6267a074bca95d..9c61bd1ed705726a66068c1b351a56e9158666e3 100644 --- a/game/modules/tome/data/talents/techniques/buckler-training.lua +++ b/game/modules/tome/data/talents/techniques/buckler-training.lua @@ -35,8 +35,11 @@ newTalent { return self:combatLimit(self:getTalentLevel(t)*10+self:getCun()*0.5, 50, 5, 15, 25, 100) end, -- called by _M:combatArmorHardiness - getHardiness = function(self, t) - return 0 --self:getTalentLevel(t) * 4; + getArmorHardiness = function(self, t) + return self:combatTalentLimit(t, 30, 10, 25) + end, + getArmour = function(self, t) + return self:combatTalentLimit(t, 20, 3, 15) end, -- called by Combat.attackTargetWith shouldEvade = function(self, t) @@ -56,11 +59,13 @@ newTalent { end, info = function(self, t) local block = t.chance(self, t) - local armor = t.getHardiness(self, t) + local armour = t.getArmour(self,t) + local hardiness = t.getArmorHardiness(self, t) return ([[Allows shields to be equipped, using Cunning instead of strength as a requirement. When you are attacked in melee, you have a %d%% chance to deflect the attack with your shield, completely evading it. + In addition, as long as you are wearing armour no heavier than leather, you gain %d Armour and %d%% Armour hardiness. The chance to deflect increases with your Cunning.]]) - :format(block, armor) + :format(block, armour, hardiness) end, } diff --git a/game/modules/tome/data/talents/techniques/duelist.lua b/game/modules/tome/data/talents/techniques/duelist.lua index 751f57f01c0218f9cee988cd994b483d4a960f4d..64ee2651144f5c537716272f9f50cc82ff9a480a 100644 --- a/game/modules/tome/data/talents/techniques/duelist.lua +++ b/game/modules/tome/data/talents/techniques/duelist.lua @@ -219,8 +219,10 @@ newTalent{ -- Attack local dam = t.getDamage(self,t) local spd, hitted, dmg = self:attackTargetWith(target, offweapon.combat, nil, self:getOffHandMult(offweapon.combat, dam)) - if hitted then + if hitted and target:canBe("disarm") then target:setEffect(target.EFF_DISARMED, t.getDuration(self, t), {apply_power=self:combatAttack()}) + else + game.logSeen(target, "%s resists the blow!", target.name:capitalize()) end return true end, diff --git a/game/modules/tome/data/talents/techniques/skirmisher-slings.lua b/game/modules/tome/data/talents/techniques/skirmisher-slings.lua index 346707eee4089d7ef0c8740b69bcacace8a356eb..ab50960e198677c75d0b9385b17e190f87799c70 100644 --- a/game/modules/tome/data/talents/techniques/skirmisher-slings.lua +++ b/game/modules/tome/data/talents/techniques/skirmisher-slings.lua @@ -117,11 +117,11 @@ newTalent { end, on_pre_use = function(self, t, silent) return archerPreUse(self, t, silent, "sling") end, damage_multiplier = function(self, t) - return self:combatTalentWeaponDamage(t, 0.2, 0.8) + return self:combatTalentWeaponDamage(t, 0.4, 1.2) end, -- Maximum number of shots fired. limit_shots = function(self, t) - return math.floor(self:combatTalentScale(t, 6, 11, "log")) + return math.floor(self:combatTalentScale(t, 9, 16, "log")) end, action = function(self, t) -- Get list of possible targets, possibly doubled. diff --git a/game/modules/tome/data/talents/techniques/tireless-combatant.lua b/game/modules/tome/data/talents/techniques/tireless-combatant.lua index ffa54c972b010ac5fcc23956316726a83cda381f..f9d1e98262996d71dffe0372c64ed2357be877d6 100644 --- a/game/modules/tome/data/talents/techniques/tireless-combatant.lua +++ b/game/modules/tome/data/talents/techniques/tireless-combatant.lua @@ -19,7 +19,7 @@ newTalent { short_name = "SKIRMISHER_BREATHING_ROOM", name = "Breathing Room", type = {"technique/tireless-combatant", 1}, - require = techs_wil_req1, + require = techs_strdex_req1, mode = "passive", points = 5, getRestoreRate = function(self, t) @@ -81,7 +81,7 @@ newTalent { cooldown = 10, sustain_stamina = 0, no_energy = true, - require = techs_wil_req2, + require = techs_strdex_req2, tactical = { STAMINA = 2 }, random_ego = "utility", activate = function(self, t) @@ -113,7 +113,7 @@ newTalent { short_name = "SKIRMISHER_DAUNTLESS_CHALLENGER", name = "Dauntless Challenger", type = {"technique/tireless-combatant", 3}, - require = techs_wil_req3, + require = techs_strdex_req3, mode = "passive", points = 5, getStaminaRate = function(self, t) @@ -171,7 +171,7 @@ newTalent { short_name = "SKIRMISHER_THE_ETERNAL_WARRIOR", name = "The Eternal Warrior", type = {"technique/tireless-combatant", 4}, - require = techs_wil_req4, + require = techs_strdex_req4, mode = "passive", points = 5, getResist = function(self, t) diff --git a/game/modules/tome/data/talents/uber/dex.lua b/game/modules/tome/data/talents/uber/dex.lua index 6b2a7c6997d3910f7c9373d73644c3f5ca33c04d..537b418508f619c57a9c37765172b539714af6ae 100644 --- a/game/modules/tome/data/talents/uber/dex.lua +++ b/game/modules/tome/data/talents/uber/dex.lua @@ -128,7 +128,7 @@ uberTalent{ uberTalent{ name = "Windtouched Speed", mode = "passive", - require = { special={desc="Know at least 20 talent levels of equilibrium-using talents", fct=function(self) return knowRessource(self, "equilibrium", 20) end} }, + require = { special={desc="Know at least 10 talent levels of equilibrium-using talents", fct=function(self) return knowRessource(self, "equilibrium", 10) end} }, on_learn = function(self, t) self:attr("global_speed_add", 0.2) self:attr("avoid_pressure_traps", 1) diff --git a/game/modules/tome/data/timed_effects/magical.lua b/game/modules/tome/data/timed_effects/magical.lua index 83bcdd76e98f56d01181914969eeff991199b831..459dcb673b507982d7e6b757919f734eccda91e4 100644 --- a/game/modules/tome/data/timed_effects/magical.lua +++ b/game/modules/tome/data/timed_effects/magical.lua @@ -4031,3 +4031,95 @@ newEffect{ deactivate = function(self, eff) end, } + + +newEffect{ + name = "PACIFICATION_HEX", image = "talents/pacification_hex.png", + desc = "Pacification Hex", + long_desc = function(self, eff) return ("The target is hexed, granting it %d%% chance each turn to be dazed for 3 turns."):format(eff.chance) end, + type = "magical", + subtype = { hex=true, dominate=true }, + status = "detrimental", + parameters = {chance=10, power=10}, + on_gain = function(self, err) return "#Target# is hexed!", "+Pacification Hex" end, + on_lose = function(self, err) return "#Target# is free from the hex.", "-Pacification Hex" end, + -- Damage each turn + on_timeout = function(self, eff) + if not self:hasEffect(self.EFF_DAZED) and rng.percent(eff.chance) and self:canBe("stun") then + self:setEffect(self.EFF_DAZED, 3, {}) + if not self:checkHit(eff.power, self:combatSpellResist(), 0, 95, 15) then eff.dur = 0 end + end + end, + activate = function(self, eff) + if self:canBe("stun") then + self:setEffect(self.EFF_DAZED, 3, {}) + end + if core.shader.active() then + local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {shader=true, oversize=0.5, a=225, appear=8, speed=0, img="pacification_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end + end + end, + deactivate = function(self, eff) + if eff.particle then self:removeParticles(eff.particle) end + end, +} + +newEffect{ + name = "BURNING_HEX", image = "talents/burning_hex.png", + desc = "Burning Hex", + long_desc = function(self, eff) return ("The target is hexed. Each time it uses an ability it takes %0.2f fire damage, and talent cooldowns are increased by %s plus 1 turn."): + format(eff.dam, eff.power and ("%d%%"):format((eff.power-1)*100) or "") + end, + type = "magical", + subtype = { hex=true, fire=true }, + status = "detrimental", + -- _M:getTalentCooldown(t) in mod.class.Actor.lua references this table to compute cooldowns + parameters = {dam=10, power = 1}, + on_gain = function(self, err) return "#Target# is hexed!", "+Burning Hex" end, + on_lose = function(self, err) return "#Target# is free from the hex.", "-Burning Hex" end, +} + +newEffect{ + name = "EMPATHIC_HEX", image = "talents/empathic_hex.png", + desc = "Empathic Hex", + long_desc = function(self, eff) return ("The target is hexed, creating an empathic bond with its victims. It takes %d%% feedback damage from all damage done."):format(eff.power) end, + type = "magical", + subtype = { hex=true, dominate=true }, + status = "detrimental", + parameters = { power=10 }, + on_gain = function(self, err) return "#Target# is hexed.", "+Empathic Hex" end, + on_lose = function(self, err) return "#Target# is free from the hex.", "-Empathic hex" end, + activate = function(self, eff) + eff.tmpid = self:addTemporaryValue("martyrdom", eff.power) + if core.shader.active() then + local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {toback=true, shader=true, oversize=0.5, a=225, appear=8, speed=0, img="empathic_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end + end + end, + deactivate = function(self, eff) + if eff.particle then self:removeParticles(eff.particle) end + self:removeTemporaryValue("martyrdom", eff.tmpid) + end, +} + +newEffect{ + name = "DOMINATION_HEX", image = "talents/domination_hex.png", + desc = "Domination Hex", + long_desc = function(self, eff) return ("The target is hexed, temporarily changing its faction to %s."):format(engine.Faction.factions[eff.faction].name) end, + type = "magical", + subtype = { hex=true, dominate=true }, + status = "detrimental", + parameters = {}, + on_gain = function(self, err) return "#Target# is hexed.", "+Domination Hex" end, + on_lose = function(self, err) return "#Target# is free from the hex.", "-Domination hex" end, + activate = function(self, eff) + self:setTarget() -- clear ai target + eff.olf_faction = self.faction + self.faction = eff.src.faction + if core.shader.active() then + local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {shader=true, oversize=1, a=225, appear=8, speed=0, img="domination_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end + end + end, + deactivate = function(self, eff) + if eff.particle then self:removeParticles(eff.particle) end + self.faction = eff.olf_faction + end, +} diff --git a/game/modules/tome/data/timed_effects/mental.lua b/game/modules/tome/data/timed_effects/mental.lua index f97b6ecf13a7261b48df623ac9839c087a9866a6..47c40890362382aa539fbfadf570cde47041d348 100644 --- a/game/modules/tome/data/timed_effects/mental.lua +++ b/game/modules/tome/data/timed_effects/mental.lua @@ -1601,6 +1601,7 @@ newEffect{ if rng.percent(chance) then if self:attr("sleep") or self:checkHit(eff.src:combatMindpower(), self:combatMentalResist(), 0, 95, 5) then t.summon_inner_demons(eff.src, self, t) + self:removeEffectsFilter({subtype={["sleep"] = true}}, 3) -- Allow the player to actually react to one of the biggest threats in the game before 50 more spawn else eff.dur = 0 end @@ -1608,98 +1609,6 @@ newEffect{ end, } -newEffect{ - name = "PACIFICATION_HEX", image = "talents/pacification_hex.png", - desc = "Pacification Hex", - long_desc = function(self, eff) return ("The target is hexed, granting it %d%% chance each turn to be dazed for 3 turns."):format(eff.chance) end, - type = "mental", - subtype = { hex=true, dominate=true }, - status = "detrimental", - parameters = {chance=10, power=10}, - on_gain = function(self, err) return "#Target# is hexed!", "+Pacification Hex" end, - on_lose = function(self, err) return "#Target# is free from the hex.", "-Pacification Hex" end, - -- Damage each turn - on_timeout = function(self, eff) - if not self:hasEffect(self.EFF_DAZED) and rng.percent(eff.chance) and self:canBe("stun") then - self:setEffect(self.EFF_DAZED, 3, {}) - if not self:checkHit(eff.power, self:combatSpellResist(), 0, 95, 15) then eff.dur = 0 end - end - end, - activate = function(self, eff) - if self:canBe("stun") then - self:setEffect(self.EFF_DAZED, 3, {}) - end - if core.shader.active() then - local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {shader=true, oversize=0.5, a=225, appear=8, speed=0, img="pacification_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end - end - end, - deactivate = function(self, eff) - if eff.particle then self:removeParticles(eff.particle) end - end, -} - -newEffect{ - name = "BURNING_HEX", image = "talents/burning_hex.png", - desc = "Burning Hex", - long_desc = function(self, eff) return ("The target is hexed. Each time it uses an ability it takes %0.2f fire damage, and talent cooldowns are increased by %s plus 1 turn."): - format(eff.dam, eff.power and ("%d%%"):format((eff.power-1)*100) or "") - end, - type = "mental", - subtype = { hex=true, fire=true }, - status = "detrimental", - -- _M:getTalentCooldown(t) in mod.class.Actor.lua references this table to compute cooldowns - parameters = {dam=10, power = 1}, - on_gain = function(self, err) return "#Target# is hexed!", "+Burning Hex" end, - on_lose = function(self, err) return "#Target# is free from the hex.", "-Burning Hex" end, -} - -newEffect{ - name = "EMPATHIC_HEX", image = "talents/empathic_hex.png", - desc = "Empathic Hex", - long_desc = function(self, eff) return ("The target is hexed, creating an empathic bond with its victims. It takes %d%% feedback damage from all damage done."):format(eff.power) end, - type = "mental", - subtype = { hex=true, dominate=true }, - status = "detrimental", - parameters = { power=10 }, - on_gain = function(self, err) return "#Target# is hexed.", "+Empathic Hex" end, - on_lose = function(self, err) return "#Target# is free from the hex.", "-Empathic hex" end, - activate = function(self, eff) - eff.tmpid = self:addTemporaryValue("martyrdom", eff.power) - if core.shader.active() then - local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {toback=true, shader=true, oversize=0.5, a=225, appear=8, speed=0, img="empathic_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end - end - end, - deactivate = function(self, eff) - if eff.particle then self:removeParticles(eff.particle) end - self:removeTemporaryValue("martyrdom", eff.tmpid) - end, -} - -newEffect{ - name = "DOMINATION_HEX", image = "talents/domination_hex.png", - desc = "Domination Hex", - long_desc = function(self, eff) return ("The target is hexed, temporarily changing its faction to %s."):format(engine.Faction.factions[eff.faction].name) end, - type = "mental", - subtype = { hex=true, dominate=true }, - status = "detrimental", - parameters = {}, - on_gain = function(self, err) return "#Target# is hexed.", "+Domination Hex" end, - on_lose = function(self, err) return "#Target# is free from the hex.", "-Domination hex" end, - activate = function(self, eff) - self:setTarget() -- clear ai target - eff.olf_faction = self.faction - self.faction = eff.src.faction - if core.shader.active() then - local h1x, h1y = self:attachementSpot("head", true) if h1x then eff.particle = self:addParticles(Particles.new("circle", 1, {shader=true, oversize=1, a=225, appear=8, speed=0, img="domination_hex_debuff_aura", base_rot=0, radius=0, x=h1x, y=h1y})) end - end - end, - deactivate = function(self, eff) - if eff.particle then self:removeParticles(eff.particle) end - self.faction = eff.olf_faction - end, -} - - newEffect{ name = "DOMINATE_ENTHRALL", image = "talents/yeek_will.png", desc = "Enthralled", diff --git a/game/modules/tome/data/timed_effects/other.lua b/game/modules/tome/data/timed_effects/other.lua index 9022d7ad6913e9bca2ead613c5e20ae5657cd5f8..a474e4a6a0b53bab9dd00954c67c7347d19ce752 100644 --- a/game/modules/tome/data/timed_effects/other.lua +++ b/game/modules/tome/data/timed_effects/other.lua @@ -1656,52 +1656,28 @@ newEffect{ local spawn_time = 2 if eff.dur%spawn_time == 0 then - -- Fine space + -- Find space local x, y = util.findFreeGrid(eff.target.x, eff.target.y, 5, true, {[Map.ACTOR]=true}) if not x then - game.logPlayer(self, "Not enough space to summon!") + game.logPlayer(self, "You could not find enough space to form a dream projection...") return end - - -- Create a clone for later spawning - local m = require("mod.class.NPC").new(eff.target:cloneFull{ - shader = "shadow_simulacrum", - shader_args = { color = {0.0, 1, 1}, base = 0.6 }, - no_drops = true, keep_inven_on_death = false, - faction = eff.target.faction, - summoner = eff.target, summoner_gain_exp=true, - ai_target = {actor=nil}, + local m = require("mod.class.NPC").new(eff.target:cloneActor{ + shader = "shadow_simulacrum", shader_args = { color = {0.0, 1, 1}, base = 0.6 }, + is_psychic_projection = true, + summoner = eff.target, summoner_gain_exp=true, exp_worth=0, + _rst_full=true, can_change_level=table.NIL_MERGE, can_change_zone=table.NIL_MERGE, + ai_target={actor=table.NIL_MERGE}, + max_level = eff.target.level, + life = util.bound(eff.target.life, eff.target.die_at, eff.target.max_life), ai = "summoned", ai_real = "tactical", - ai_state = eff.target.ai_state or { ai_move="move_complex", talent_in=1 }, + ai_state={ ai_move="move_complex", talent_in=1, ally_compassion = 10}, name = eff.target.name.."'s dream projection", }) - - -- Change some values; most of this is typical clone protection stuff - m.ai_state.ally_compassion = 10 - m:removeAllMOs() - m.make_escort = nil - m.on_added_to_level = nil - m._rst_full = true - m.forceLevelup = function() end - m.on_acquire_target = nil - m.seen_by = nil - m.can_talk = nil - m.puuid = nil - m.on_takehit = nil - m.exp_worth = 0 - m.no_inventory_access = true - m.clone_on_hit = nil - m.player = nil - m.energy.value = 0 - m.max_life = m.max_life - m.life = util.bound(m.life, 0, m.max_life) - m.remove_from_party_on_death = true + if not eff.target:attr("lucid_dreamer") then m.inc_damage.all = (m.inc_damage.all or 0) - 50 end - - -- special stuff - m.is_psychic_projection = true m.lucid_dreamer = 1 -- Remove some talents @@ -1711,15 +1687,15 @@ newEffect{ if (t.no_npc_use and not t.allow_temporal_clones) or t.remove_on_clone then tids[#tids+1] = t end end for i, t in ipairs(tids) do - if t.mode == "sustained" and m:isTalentActive(t.id) then m:forceUseTalent(t.id, {ignore_energy=true, silent=true}) end m:unlearnTalentFull(t.id) end -- remove imprisonment - m.invulnerable = m.invulnerable - 1 - m.time_prison = m.time_prison - 1 - m.no_timeflow = m.no_timeflow - 1 - m.status_effect_immune = m.status_effect_immune - 1 + m:attr("invulnerable", -1) + m:attr("time_prison", -1) + m:attr("no_timeflow", -1) + m:attr("status_effect_immune", -1) + m:removeParticles(eff.particle) m:removeTimedEffectsOnClone() @@ -2314,7 +2290,10 @@ newEffect{ newEffect{ name = "ANTIMAGIC_DISRUPTION", desc = "Antimagic Disruption", - long_desc = function(self, eff) return ("Your arcane powers are disrupted by your antimagic equipment."):format() end, + long_desc = function(self, eff) + local chance = self:attr("spell_failure") or 0 + return ("Your arcane powers are disrupted by your antimagic equipment. Arcane talents fail %d%% of the time and arcane sustains have a %0.1f%% chance to deactivate each turn."):format(chance, chance/10) + end, type = "other", subtype = { antimagic=true }, no_stop_enter_worlmap = true, diff --git a/game/modules/tome/data/zones/ancient-elven-ruins/npcs.lua b/game/modules/tome/data/zones/ancient-elven-ruins/npcs.lua index e74ab49021c42d6d24ed920d5b151754f6b3879c..6e00226f34cc055c16175362bcdde2e96fc33474 100644 --- a/game/modules/tome/data/zones/ancient-elven-ruins/npcs.lua +++ b/game/modules/tome/data/zones/ancient-elven-ruins/npcs.lua @@ -48,16 +48,21 @@ newEntity{ define_as = "GREATER_MUMMY_LORD", infravision = 10, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, }, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", not_properties={"twohanded"}}, + OFFHAND = {special=shield_special}, + BODY = {type="armor", special=function(e) return e.subtype=="mummy" or e.subtype=="heavy" or e.subtype=="massive" end}, + }, equipment = resolvers.equip{ {type="weapon", subtype="longsword", defined="LONGSWORD_WINTERTIDE", random_art_replace={chance=75}, autoreq=true}, {type="armor", subtype="shield", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, - {type="armor", subtype="mummy", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, - {type="armor", subtype="head", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, + {type="armor", subtype="mummy", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true, base_list="mod.class.Object:/data/zones/ancient-elven-ruins/objects.lua"}, + {type="armor", subtype="head", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true, base_list="mod.class.Object:/data/zones/ancient-elven-ruins/objects.lua"}, }, - resolvers.drops{chance=100, nb=4, {tome_drops="boss"} }, - + resolvers.drops{{tome_drops="boss", type="armor", subtype="heavy", forbid_power_source={antimagic=true}, autoreq=true,}}, + resolvers.drops{chance=100, nb=3, {tome_drops="boss"} }, resolvers.talents{ - [Talents.T_ARMOUR_TRAINING]={base=2, every=10, max=5}, + [Talents.T_ARMOUR_TRAINING]={base=3, every=10, max=5}, [Talents.T_SHIELD_PUMMEL]={base=5, every=5, max=8}, [Talents.T_ASSAULT]={base=4, every=5, max=7}, [Talents.T_OVERPOWER]={base=5, every=5, max=8}, @@ -95,10 +100,11 @@ newEntity{ base = "BASE_NPC_MUMMY", ai_state = { talent_in=4, }, stats = { mag=25, wil=20, }, infravision = 10, - + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="greatsword", forbid_power_source={antimagic=true}, autoreq=true}, - {type="armor", subtype="mummy", forbid_power_source={antimagic=true}, autoreq=true}, +-- {type="armor", subtype="mummy", forbid_power_source={antimagic=true}, autoreq=true, base_list="mod.class.Object:/data/zones/ancient-elven-ruins/objects.lua"}, + {type="armor", subtype="mummy", force_drop=true, forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.talents{ [Talents.T_STUNNING_BLOW]={base=2, every=7, max=6}, @@ -121,6 +127,7 @@ newEntity{ base = "BASE_NPC_MUMMY", infravision = 10, resolvers.equip{ +-- {type="armor", subtype="mummy", force_drop=true, forbid_power_source={antimagic=true}, autoreq=true, base_list="mod.class.Object:/data/zones/ancient-elven-ruins/objects.lua"}, {type="armor", subtype="mummy", force_drop=true, forbid_power_source={antimagic=true}, autoreq=true}, }, autolevel = "caster", @@ -144,7 +151,8 @@ newEntity{ base = "BASE_NPC_MUMMY", infravision = 10, resolvers.equip{ - {type="armor", subtype="mummy", forbid_power_source={antimagic=true}, autoreq=true}, +-- {type="armor", subtype="mummy", forbid_power_source={antimagic=true}, autoreq=true, base_list="mod.class.Object:/data/zones/ancient-elven-ruins/objects.lua"}, + {type="armor", subtype="mummy", force_drop=true, forbid_power_source={antimagic=true}, autoreq=true}, }, autolevel = "ghoul", resolvers.talents{ diff --git a/game/modules/tome/data/zones/ardhungol/npcs.lua b/game/modules/tome/data/zones/ardhungol/npcs.lua index 1b6eab08df978f363d651a89b16169ce07baad0c..2ed23123ab25f3ed69194f482ccc74fc979381b1 100644 --- a/game/modules/tome/data/zones/ardhungol/npcs.lua +++ b/game/modules/tome/data/zones/ardhungol/npcs.lua @@ -105,7 +105,7 @@ newEntity{ base = "BASE_NPC_SPIDER", newEntity{ base = "BASE_NPC_SPIDER", subtype = "shiaak", name = "shiaak venomblade", color=colors.GREEN, - desc = [[A strange looking humanoid, covered in black chitinous skin. He dual wields sinuous daggers and seems bend on plunging them in your body.]], + desc = [[A strange looking humanoid, covered in black chitinous skin. He dual wields sinuous daggers and seems bent on plunging them in your body.]], resolvers.nice_tile{tall=1}, level_range = {35, nil}, exp_worth = 1, rarity = 4, diff --git a/game/modules/tome/data/zones/charred-scar/npcs.lua b/game/modules/tome/data/zones/charred-scar/npcs.lua index 28d350bb5a5136a2bff498e2396c69cf4c35653d..5566c542003b5078059c01f59d073cc18e2d3404 100644 --- a/game/modules/tome/data/zones/charred-scar/npcs.lua +++ b/game/modules/tome/data/zones/charred-scar/npcs.lua @@ -32,6 +32,7 @@ newEntity{ combat = { dam=resolvers.rngavg(1,2), atk=2, apr=0, dammod={str=0.4} }, + resolvers.auto_equip_filters("Sun Paladin"), body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, QUIVER=1 }, lite = 3, diff --git a/game/modules/tome/data/zones/conclave-vault/npcs.lua b/game/modules/tome/data/zones/conclave-vault/npcs.lua index 5d1b30c22740b4edfc9f5785cac80e091ca31383..dfc61cd65bff75e6b0049ca9b9abb7a8c1f47980 100644 --- a/game/modules/tome/data/zones/conclave-vault/npcs.lua +++ b/game/modules/tome/data/zones/conclave-vault/npcs.lua @@ -108,6 +108,7 @@ newEntity{ base = "BASE_NPC_OGRE", define_as = "OGRE_SENTRY", max_life = resolvers.rngavg(110,120), life_rating = 13, blind_immune = 1, + resolvers.auto_equip_filters("Berserker"), resolvers.equip{{type="weapon", subtype="greatsword", forbid_power_source={antimagic=true}, autoreq=true} }, resolvers.talents{ [Talents.T_STUNNING_BLOW]={base=3, every=4, max=8}, @@ -151,8 +152,9 @@ newEntity{ base = "BASE_NPC_OGRE", define_as = "HEALER_ASTELRID", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, TOOL=1 }, + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ - {defined="ASTELRID_CLUBSTAFF"}, + {defined="ASTELRID_CLUBSTAFF", replace_unique={type="weapon", subtype="greatmaul", tome_drops="boss", forbid_power_source={antimagic=true}}, autoreq=true}, {type="armor", subtype="cloth", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="head", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="feet", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, diff --git a/game/modules/tome/data/zones/daikara/npcs.lua b/game/modules/tome/data/zones/daikara/npcs.lua index d2393cb2ed1353d3381cc5e993f3605b8802153d..82ede1c6b878bad7acf43569f4af551e7aa16d1a 100644 --- a/game/modules/tome/data/zones/daikara/npcs.lua +++ b/game/modules/tome/data/zones/daikara/npcs.lua @@ -173,7 +173,7 @@ newEntity{ base="BASE_NPC_ORC_GRUSHNAK", define_as = "MASSOK", body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, FEET=1, FINGER=2, NECK=1 }, resists = { [DamageType.COLD] = 100 }, - + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="massive", force_drop=true, tome_drops="boss", autoreq=true}, diff --git a/game/modules/tome/data/zones/demon-plane/npcs.lua b/game/modules/tome/data/zones/demon-plane/npcs.lua index ee9eba3081870bf22d80033279ffeb992b52c38e..90203ae34c5ac9b8fb2960bcd385f4e2cee11148 100644 --- a/game/modules/tome/data/zones/demon-plane/npcs.lua +++ b/game/modules/tome/data/zones/demon-plane/npcs.lua @@ -52,7 +52,7 @@ newEntity{ define_as = "DRAEBOR", body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, FEET = 1 }, resolvers.drops{chance=100, nb=4, {tome_drops="boss"} }, equipment = resolvers.equip{ - {type="armor", subtype="feet", defined="BOOTS_OF_PHASING", autoreq=true}, + {type="armor", subtype="feet", defined="BOOTS_OF_PHASING", random_art_replace={chance=75}, autoreq=true}, }, summon = { diff --git a/game/modules/tome/data/zones/dreadfell/npcs.lua b/game/modules/tome/data/zones/dreadfell/npcs.lua index 2b1b773a4a483220289c93e3cf529eadef5f85b9..03a752e76ac62faa85ebdd38cf26d35936711f93 100644 --- a/game/modules/tome/data/zones/dreadfell/npcs.lua +++ b/game/modules/tome/data/zones/dreadfell/npcs.lua @@ -50,6 +50,7 @@ newEntity{ define_as = "THE_MASTER", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, NECK=1, }, + resolvers.auto_equip_filters("Berzerker"), equipment = resolvers.equip{ {type="weapon", subtype="greatsword", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="heavy", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, @@ -141,6 +142,7 @@ newEntity{ define_as = "PALE_DRAKE", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, NECK=1, }, + resolvers.auto_equip_filters("Archmage"), equipment = resolvers.equip{ {type="weapon", subtype="staff", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="cloth", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, @@ -210,6 +212,7 @@ What proud hero of renown was this before he was condemned to such a terrible fa stats = { str=30, dex=20, con=30 }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, HANDS=1, FEET=1, }, + resolvers.auto_equip_filters("Bulwark"), equipment = resolvers.equip{ {type="weapon", subtype="mace", ego_chance=100, autoreq=true, forbid_power_source={antimagic=true}, force_drop=true,}, {type="armor", subtype="shield", ego_chance=100, autoreq=true, forbid_power_source={antimagic=true}, force_drop=true,}, @@ -240,7 +243,7 @@ What proud hero of renown was this before he was condemned to such a terrible fa [Talents.T_SHIELD_EXPERTISE]=6, [Talents.T_THICK_SKIN]={base=3, every=5, max=5}, - [Talents.T_ARMOUR_TRAINING]={base=2, every=8, max=5}, + [Talents.T_ARMOUR_TRAINING]={base=3, every=8, max=5}, [Talents.T_WEAPONS_MASTERY]={base=2, every=10, max=5}, [Talents.T_WEAPON_COMBAT]={base=2, every=10, max=5}, @@ -356,6 +359,11 @@ There is a cunning air to his hollow skull, and his empty sockets reveal nothing stats = { str=20, dex=20, cun=10, wil=40 }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, QUIVER=1 }, + resolvers.auto_equip_filters{ + MAINHAND = {type="weapon", subtype="sling"}, + OFFHAND = {type="weapon", subtype="dagger"}, + QUIVER={type="ammo", subtype="shot"} + }, equipment = resolvers.equip{ {type="weapon", subtype="sling", defined="HARESKIN_SLING", random_art_replace={chance=0}, autoreq=true, tome_drops="boss"}, {type="weapon", subtype="dagger", ego_chance=100, autoreq=true, forbid_power_source={antimagic=true}, force_drop=true}, diff --git a/game/modules/tome/data/zones/golem-graveyard/npcs.lua b/game/modules/tome/data/zones/golem-graveyard/npcs.lua index b54f074a38b1effc651c278f93ba94f862625ab6..70b597afa43e0b2ba5e22fe12c8c4957c7c44f29 100644 --- a/game/modules/tome/data/zones/golem-graveyard/npcs.lua +++ b/game/modules/tome/data/zones/golem-graveyard/npcs.lua @@ -29,8 +29,8 @@ newEntity{ define_as = "ATAMATHON", base = "BASE_NPC_CONSTRUCT", resolvers.nice_tile{image="invis.png", add_mos = {{image="npc/construct_golem_athamathon_the_giant_golem.png", display_h=2, display_y=-1}}}, desc = [[This giant golem was constructed by the Halflings during the Pyre Wars to fight the orcs, but was felled by Garkul the Devourer. Someone foolish has tried to reconstruct it, but has lost control of it, and now it rampages in search of its original creators, who are long dead. Its body is made of marble, its joints of solid voratun, and its eyes of purest ruby. At over 40 feet tall, it towers above you, and its crimson orbs seem to glow with rage.]], level_range = {70, nil}, exp_worth = 2, - max_life = 350, life_rating = 40, fixed_rating = true, - life_regen = 0, + max_life = 5000, life_rating = 60, fixed_rating = true, + life_regen = 150, stats = { str=35, dex=10, cun=8, mag=30, con=30 }, rank = 5, size_category = 5, @@ -38,13 +38,25 @@ newEntity{ define_as = "ATAMATHON", base = "BASE_NPC_CONSTRUCT", instakill_immune = 1, move_others=true, + emote_random = resolvers.emote_random{ chance=20, + "DESTROY!", + "LIFE-ENDING SYSTEMS ACTIVATED!", + "GLORY TO THE HALFLINGS!", + "YOUR DEATH IS NECESSARY", + "ACTIVATING PAIN GIVING SUBMODULES!", + "YOUR LIFE WILL END, PLEASE DO NOT RESIST!", + "RESISTANCE IS FUTILE, YOUR WILL BE EXTERMINATED!", + "PLEASE STAY STEADY AS YOU ARE ERASED FROM THE WORLD!", + "EXECUTE PHASE COMMENCING!", + }, + body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, GEM=4 }, resolvers.equip{ {type="weapon", subtype="greatmaul", tome_drops="boss", tome_mod="uvault", forbid_power_source={antimagic=true}, autoreq=true }, {type="armour", subtype="massive", tome_drops="boss", tome_mod="uvault", forbid_power_source={antimagic=true}, autoreq=true }, }, - combat_armor = 50, - combat_def = 30, + combat_armor = 70, + combat_def = 50, resolvers.drops{chance=100, nb=7, {type="gem"} }, resolvers.drops{chance=100, nb=2, {name="voratun amulet", ego_chance=-1000} }, resolvers.drops{chance=100, nb=4, {name="voratun ring", ego_chance=-1000} }, diff --git a/game/modules/tome/data/zones/grushnak-pride/npcs.lua b/game/modules/tome/data/zones/grushnak-pride/npcs.lua index 791ef56b8aa734ab774e1b24eb17e0fa1aa778e7..c155e36c233c6566da4157f8ae5c420bddb413b5 100644 --- a/game/modules/tome/data/zones/grushnak-pride/npcs.lua +++ b/game/modules/tome/data/zones/grushnak-pride/npcs.lua @@ -49,7 +49,7 @@ newEntity{ base="BASE_NPC_ORC_GRUSHNAK", define_as = "GRUSHNAK", resolvers.inscriptions(4, "infusion"), body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, FEET=1, FINGER=2, NECK=1, TOOL=1 }, - + resolvers.auto_equip_filters("Bulwark"), resolvers.equip{ {type="weapon", subtype="waraxe", force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="shield", force_drop=true, tome_drops="boss", autoreq=true}, diff --git a/game/modules/tome/data/zones/high-peak/npcs.lua b/game/modules/tome/data/zones/high-peak/npcs.lua index 356fe0cfebd759170266d08680c51b9d66a6fca4..5777a664210bb6db824fc6f5729c86b6c1a753f2 100644 --- a/game/modules/tome/data/zones/high-peak/npcs.lua +++ b/game/modules/tome/data/zones/high-peak/npcs.lua @@ -81,6 +81,7 @@ newEntity{ resists = { all = 40, }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, FEET=1 }, + resolvers.auto_equip_filters("Archmage", true), resolvers.equip{ {type="weapon", subtype="staff", defined="STAFF_ABSORPTION_AWAKENED", autoreq=true}, {type="armor", subtype="cloth", forbid_power_source={antimagic=true}, force_drop=true, tome_drops="boss", autoreq=true}, @@ -92,6 +93,7 @@ newEntity{ resolvers.talents{ [Talents.T_STAFF_MASTERY]={base=5, every=8}, + [Talents.T_ARMOUR_TRAINING]=1, [Talents.T_STONE_SKIN]={base=7, every=6}, [Talents.T_QUICKEN_SPELLS]={base=7, every=6}, [Talents.T_SPELLCRAFT]={base=7, every=6}, @@ -159,6 +161,10 @@ newEntity{ resists = { all = 45, }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, FEET=1, HEAD=1, HANDS=1 }, + resolvers.auto_equip_filters("Reaver"), + resolvers.auto_equip_filters{ + BODY = {type="armor", special=function(e) return e.subtype=="heavy" or e.subtype=="massive" end}, + }, resolvers.equip{ {type="weapon", subtype="longsword", force_drop=true, forbid_power_source={antimagic=true}, tome_drops="boss", autoreq=true}, {type="weapon", subtype="waraxe", force_drop=true, forbid_power_source={antimagic=true}, tome_drops="boss", autoreq=true}, @@ -242,6 +248,7 @@ newEntity{ define_as = "FALLEN_SUN_PALADIN_AERYN", no_auto_resists = true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, FEET=1 }, + resolvers.auto_equip_filters("Sun Paladin"), resolvers.drops{chance=100, nb=3, {tome_drops="boss"} }, resolvers.equip{ @@ -316,6 +323,7 @@ newEntity{ define_as = "HIGH_SUN_PALADIN_AERYN", resolvers.inscriptions(4, {}), body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, FEET=1 }, + resolvers.auto_equip_filters("Sun Paladin"), resolvers.drops{chance=100, nb=3, {tome_drops="boss"} }, resolvers.equip{ diff --git a/game/modules/tome/data/zones/high-peak/objects.lua b/game/modules/tome/data/zones/high-peak/objects.lua index 56822df0fd2742d61373675d44294d89b627bc0a..35b848b4febbbd31138e409cb798af94dac48720 100644 --- a/game/modules/tome/data/zones/high-peak/objects.lua +++ b/game/modules/tome/data/zones/high-peak/objects.lua @@ -67,6 +67,7 @@ newEntity{ define_as = "STAFF_ABSORPTION_AWAKENED", base="BASE_STAFF", display = "\\", color=colors.VIOLET, image = "object/artifact/staff_absorption.png", moddable_tile = "special/%s_awaken_staff_of_absorbtion", encumber = 7, + material_level = 5, plot=true, desc = [[Carved with runes of power, this staff seems to have been made long ago, yet it bears no signs of tarnish. Light around it seems to dim and you can feel its tremendous power simply by touching it. diff --git a/game/modules/tome/data/zones/infinite-dungeon/zone.lua b/game/modules/tome/data/zones/infinite-dungeon/zone.lua index 62448f25d337b63daa7a7638516748ad4624c818..a81e0fb4061150d1cd785794224aba59a2b1c513 100644 --- a/game/modules/tome/data/zones/infinite-dungeon/zone.lua +++ b/game/modules/tome/data/zones/infinite-dungeon/zone.lua @@ -1,5 +1,3 @@ - - -- ToME - Tales of Maj'Eyal -- Copyright (C) 2009 - 2017 Nicolas Casalini -- @@ -211,6 +209,8 @@ return { vgrid = vgrids[vgridN] print("[Infinite Dungeon] using zone layout #", layoutN, layout.id_layout_name) table.print(layout, "\t") print("[Infinite Dungeon] using variable grid set #", vgridN, vgrid.id_grids_name) table.print(vgrid, "\t") + + if layout.rooms and game:isAddonActive("items-vault") then table.insert(layout.rooms, {"!items-vault",3}) end data.generator.map = layout @@ -245,6 +245,7 @@ return { data.generator.map.down = data.alternate_exit[1].grids.down -- exit matches destination data.generator.map.door = vgrid.door data.generator.map["'"] = vgrid.door + data.generator.map.I = "ITEMS_VAULT" data.width, data.height = vx, vy data.generator.map.width, data.generator.map.height = vx, vy @@ -357,8 +358,6 @@ return { level.data.effects = {effid} end - game.state:infiniteDungeonChallengeFinish(zone, level) - if config.settings.cheat then -- gather statistics local block_count = 0 for i = 0, level.map.w - 1 do for j = 0, level.map.h - 1 do @@ -368,5 +367,10 @@ return { print(("[Infinite Dungeon] Open space calculation: (%s, %s, %dw x %dh) space -- (open:%2.1f%%, closed:%2.1f%%)"):format(level.data.id_layout_name, level.data.id_grids_name, level.map.w, level.map.h, open, closed)) end end, + post_process_end = function(level, zone) + -- We delay it because at "post_process" the map can STILL decide to regenerate + -- and if it does, it's a new level and challenge is considered auto failed (or auto success heh) + game.state:infiniteDungeonChallengeFinish(zone, level) + end, } diff --git a/game/modules/tome/data/zones/maze/npcs.lua b/game/modules/tome/data/zones/maze/npcs.lua index 1adbffc8212b517d716bef49f5ff5db5b8de87b2..2b41b91b594415fc3946c53c744c3f783ecc6b3c 100644 --- a/game/modules/tome/data/zones/maze/npcs.lua +++ b/game/modules/tome/data/zones/maze/npcs.lua @@ -64,6 +64,7 @@ newEntity{ define_as = "HORNED_HORROR", no_breath = 1, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HANDS=1, }, + resolvers.auto_equip_filters("Brawler"), resolvers.equip{ {type="armor", subtype="hands", defined="STORM_BRINGER_GAUNTLETS", random_art_replace={chance=75}, autoreq=true}, }, @@ -113,6 +114,7 @@ newEntity{ define_as = "MINOTAUR_MAZE", instakill_immune = 1, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, }, + resolvers.auto_equip_filters("Berserker"), resolvers.equip{ {type="weapon", subtype="battleaxe", force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="head", defined="HELM_OF_GARKUL", random_art_replace={chance=75}, autoreq=true}, diff --git a/game/modules/tome/data/zones/reknor-escape/npcs.lua b/game/modules/tome/data/zones/reknor-escape/npcs.lua index 60b3879312fb6f243f735757856c804175165770..919351c6afad8999b89845d6c1e85ba6f4939926 100644 --- a/game/modules/tome/data/zones/reknor-escape/npcs.lua +++ b/game/modules/tome/data/zones/reknor-escape/npcs.lua @@ -47,6 +47,7 @@ newEntity{ define_as = "BROTOQ", inc_damage = {all=-55}, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1 }, + resolvers.auto_equip_filters("Reaver"), equipment = resolvers.equip{ {type="weapon", subtype="waraxe", defined="FAKE_SKULLCLEAVER", never_drop=true}, {type="weapon", subtype="longsword", forbid_power_source={antimagic=true}, autoreq=true}, diff --git a/game/modules/tome/data/zones/reknor/npcs.lua b/game/modules/tome/data/zones/reknor/npcs.lua index 801860452dfefb7a6d0f82a74aec48bc07e27356..0bd08046a699a421d85215886f77488ab817e04b 100644 --- a/game/modules/tome/data/zones/reknor/npcs.lua +++ b/game/modules/tome/data/zones/reknor/npcs.lua @@ -44,6 +44,7 @@ newEntity{ define_as = "GOLBUG", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, NECK=1, HEAD=1, }, + resolvers.auto_equip_filters("Bulwark"), equipment = resolvers.equip{ {type="weapon", subtype="mace", force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="shield", force_drop=true, tome_drops="boss", autoreq=true}, diff --git a/game/modules/tome/data/zones/shadow-crypt/npcs.lua b/game/modules/tome/data/zones/shadow-crypt/npcs.lua index e302bb7ba762dbd6c3f4542647e81773b6025b1f..09fd293299d0bb7b3d8b1f43ba1d8ff80f615284 100644 --- a/game/modules/tome/data/zones/shadow-crypt/npcs.lua +++ b/game/modules/tome/data/zones/shadow-crypt/npcs.lua @@ -76,27 +76,24 @@ newEntity{ base="BASE_NPC_ORC_RAK_SHOR", define_as = "CULTIST_RAK_SHOR", -- When the bone shield is taken down, copy the player if (not p or p.nb <= 0) and not self.copied_player then + local Talents = require("engine.interface.ActorTalents") local a = mod.class.NPC.new{} - a:replaceWith(game.player:resolveSource():cloneFull()) + local plr = game.player:resolveSource() + a:replaceWith(plr:cloneActor({rank=4, + level_range=self.level_range, + is_player_doomed_shade = true, + faction = self.faction, + life=plr.max_life*1.2, max_life=plr.max_life*1.2, die_at=plr.die_at*1.2, + max_level=table.NIL_MERGE, + name = "Doomed Shade of "..plr.name, + desc = ([[The Dark Side of %s, completely consumed by hate...]]):format(plr.name), + killer_message = "but nobody knew why #sex# suddenly became evil", + color_r = 150, color_g = 150, color_b = 150, + ai = "tactical", ai_state = {talent_in=1}, + })) mod.class.NPC.castAs(a) engine.interface.ActorAI.init(a, a) - a.no_drops = true - a.keep_inven_on_death = false - a.energy.value = 0 - a.player = nil - a.rank = 4 - a.name = "Doomed Shade of "..a.name - a.killer_message = "but nobody knew why #sex# suddenly became evil" - a.is_player_doomed_shade = true - a.color_r = 150 a.color_g = 150 a.color_b = 150 - a:removeAllMOs() - a.ai = "tactical" - a.puuid = nil - a.ai_state = {talent_in=1} - a.faction = self.faction a.inc_damage.all = (a.inc_damage.all or 0) - 40 - a.max_life = a.max_life * 1.2 - a.life = a.max_life a.on_die = function(self) world:gainAchievement("SHADOW_CLONE", game.player) game:setAllowedBuild("afflicted") @@ -105,38 +102,31 @@ newEntity{ base="BASE_NPC_ORC_RAK_SHOR", define_as = "CULTIST_RAK_SHOR", game.logSeen(self, "As your shade dies, the magical veil protecting the stairs out vanishes.") end - -- Remove some talents - local tids = {} - for tid, _ in pairs(a.talents) do - local t = a:getTalentFromId(tid) - if t.no_npc_use then tids[#tids+1] = t end - end - for i, t in ipairs(tids) do - if t.mode == "sustained" and a:isTalentActive(t.id) then a:forceUseTalent(t.id, {ignore_energy=true}) end - a.talents[t.id] = nil - end - - -- Add some - a.talents[a.T_UNNATURAL_BODY] = 7 - a.talents[a.T_RELENTLESS] = 7 - a.talents[a.T_FEED_POWER] = 5 - a.talents[a.T_FEED_STRENGTHS] = 5 - a.talents[a.T_DARK_TENDRILS] = 5 - a.talents[a.T_WILLFUL_STRIKE] = 7 - a.talents[a.T_REPROACH] = 5 - a.talents[a.T_CALL_SHADOWS] = 5 + -- Remove any disallowed talents + a:unlearnTalentsOnClone() + -- Add some hate-based talents + table.insert(a, resolvers.talents{ + [Talents.T_UNNATURAL_BODY]={base=5, every=10, max=7}, + [Talents.T_RELENTLESS]={base=5, every=10, max=7}, + [Talents.T_FEED_POWER]={base=5, every=10, max=5}, + [Talents.T_FEED_STRENGTHS]={base=5, every=10, max=5}, + [Talents.T_DARK_TENDRILS]={base=5, every=10, max=5}, + [Talents.T_WILLFUL_STRIKE]={base=5, every=10, max=7}, + [Talents.T_REPROACH]={base=5, every=10, max=5}, + [Talents.T_CALL_SHADOWS]={base=5, every=10, max=5}, + }) a:incStat("wil", a.level) - + a:removeTimedEffectsOnClone() local x, y = util.findFreeGrid(self.x, self.y, 10, true, {[engine.Map.ACTOR]=true}) if x and y then - game.zone:addEntity(game.level, a, "actor", x, y) - - game.logPlayer(game.player, "#GREY#The cultist looks deep in your eyes. You feel torn apart!") + self:logCombat(game.player, "#GREY#The #Source# looks deep into your eyes. You feel torn apart!") self:doEmote("Ra'kk kor merk ZUR!!!", 120) + game.zone:addEntity(game.level, a, "actor", x, y) + a:resolve() self.copied_player = true end - if a.alchemy_golem then + if plr.alchemy_golem then a.alchemy_golem = nil local t = a:getTalentFromId(a.T_REFIT_GOLEM) t.action(a, t) diff --git a/game/modules/tome/data/zones/shertul-fortress/npcs.lua b/game/modules/tome/data/zones/shertul-fortress/npcs.lua index 05be9c5cc9dbb07de9ff28e99901c5d4240e9925..66240de900ad6c58410f5176c8256afd585b56af 100644 --- a/game/modules/tome/data/zones/shertul-fortress/npcs.lua +++ b/game/modules/tome/data/zones/shertul-fortress/npcs.lua @@ -138,6 +138,7 @@ newEntity{ define_as="TRAINING_DUMMY", max_life = 300000, life_rating = 0, life_regen = 300000, never_move = 1, + knockback_immune = 1, training_dummy = 1, on_takehit = function(self, value, src, infos) local data = game.zone.training_dummies diff --git a/game/modules/tome/data/zones/slazish-fen/npcs.lua b/game/modules/tome/data/zones/slazish-fen/npcs.lua index 3718fb6be1ac179dce18a2675f1e985f3e5d1897..5910df1054e1e618162525003b15e39d27692f5a 100644 --- a/game/modules/tome/data/zones/slazish-fen/npcs.lua +++ b/game/modules/tome/data/zones/slazish-fen/npcs.lua @@ -65,6 +65,7 @@ newEntity{ base = "BASE_NPC_NAGA", define_as = "NAGA_TIDEWARDEN", level_range = {1, nil}, exp_worth = 3, rarity = 1, max_life = resolvers.rngavg(100,120), life_rating = 13, + resolvers.auto_equip_filters{MAINHAND = {subtype="trident"},}, resolvers.equip{ {type="weapon", subtype="trident", autoreq=true, force_drop=true, special_rarity="trident_rarity"}, }, @@ -82,6 +83,7 @@ newEntity{ base = "BASE_NPC_NAGA", define_as = "NAGA_TIDECALLER", rarity = 1, max_life = resolvers.rngavg(50,60), life_rating = 10, autolevel = "caster", + resolvers.auto_equip_filters("Archmage"), resolvers.equip{ {type="weapon", subtype="staff", autoreq=true}, }, @@ -99,6 +101,7 @@ newEntity{ base = "BASE_NPC_NAGA", rarity = 1, max_life = resolvers.rngavg(80,90), life_rating = 11, autolevel = "caster", + resolvers.auto_equip_filters("Archmage"), resolvers.equip{ {type="weapon", subtype="staff", autoreq=true}, }, @@ -129,9 +132,10 @@ newEntity{ base="BASE_NPC_NAGA", define_as = "ZOISLA", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1 }, + resolvers.auto_equip_filters{MAINHAND = {subtype="trident"},}, resolvers.equip{ {type="weapon", subtype="trident", autoreq=true, force_drop=true, special_rarity="trident_rarity"}, - {defined="ROBES_DEFLECTION", autoreq=true}, + {defined="ROBES_DEFLECTION", replace_unique={type="armor", subtype="cloth"}, autoreq=true}, }, resolvers.drops{chance=100, nb=1, {unique=true, not_properties={"lore"}} }, resolvers.drops{chance=100, nb=3, {tome_drops="boss"} }, diff --git a/game/modules/tome/data/zones/sludgenest/npcs.lua b/game/modules/tome/data/zones/sludgenest/npcs.lua index d1f2b53a51478b09953e9d7ad51818026883d0d7..e8824d2de8c72fdcf5f2d9d66014a86094116369 100644 --- a/game/modules/tome/data/zones/sludgenest/npcs.lua +++ b/game/modules/tome/data/zones/sludgenest/npcs.lua @@ -42,6 +42,7 @@ newEntity{ define_as = "CORRUPTED_OOZEMANCER", combat_mindcrit = -28, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1 }, + resolvers.auto_equip_filters("Oozemancer"), resolvers.equip{ {type="weapon", subtype="mindstar", forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.equip{ {type="weapon", subtype="mindstar", forbid_power_source={antimagic=true}, autoreq=true}, }, resolvers.equip{ {type="armor", subtype="cloth", forbid_power_source={antimagic=true}, autoreq=true}, }, diff --git a/game/modules/tome/data/zones/tannen-tower/npcs.lua b/game/modules/tome/data/zones/tannen-tower/npcs.lua index 408bef44b5f7fa46faf19ec34819de1892cf6f33..511c232c6dc9f405b7d7b2a6afcee4c621a143fd 100644 --- a/game/modules/tome/data/zones/tannen-tower/npcs.lua +++ b/game/modules/tome/data/zones/tannen-tower/npcs.lua @@ -49,6 +49,7 @@ newEntity{ define_as = "TANNEN", blind_immune = 1, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, QUIVER=1, }, + resolvers.auto_equip_filters("Alchemist"), equipment = resolvers.equip{ {type="weapon", subtype="staff", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, {type="armor", subtype="cloth", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, diff --git a/game/modules/tome/data/zones/telmur/npcs.lua b/game/modules/tome/data/zones/telmur/npcs.lua index 6ad3c8011c51c2da099aec37dfd0f1d48b662096..6e46b3f3868ca3230991889e6870ba506e51abb9 100644 --- a/game/modules/tome/data/zones/telmur/npcs.lua +++ b/game/modules/tome/data/zones/telmur/npcs.lua @@ -59,9 +59,12 @@ newEntity{ define_as = "SHADE_OF_TELOS", resists = {all = 25, [DamageType.COLD] = 100, [DamageType.ACID] = 100}, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, }, + resolvers.auto_equip_filters("Archmage"), resolvers.equip{ {type="weapon", subtype="staff", defined="TELOS_TOP_HALF", random_art_replace={chance=75}, autoreq=true}, - {type="weapon", subtype="staff", defined="TELOS_BOTTOM_HALF", autoreq=true}, + {type="weapon", subtype="staff", defined="TELOS_BOTTOM_HALF", autoreq=true, + replace_unique={not_properties={"slot_forbid"}, forbid_power_source={antimagic=true}, special=function(e) return e.slot == "OFFHAND" or e.offslot == "OFFHAND" end} + }, }, resolvers.drops{chance=100, nb=4, {tome_drops="boss"} }, diff --git a/game/modules/tome/data/zones/temple-of-creation/npcs.lua b/game/modules/tome/data/zones/temple-of-creation/npcs.lua index 0b93ab5aa065c0225dc4163ce7a1548a171cc68b..4ebd092fb2100a209d9ec8044bc3920a4912cc6a 100644 --- a/game/modules/tome/data/zones/temple-of-creation/npcs.lua +++ b/game/modules/tome/data/zones/temple-of-creation/npcs.lua @@ -53,11 +53,13 @@ newEntity{ define_as = "SLASUL", resists = { [DamageType.COLD] = 60, [DamageType.ACID] = 20, }, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, LITE=1 }, + resolvers.auto_equip_filters("Sun Paladin"), resolvers.equip{ {type="weapon", subtype="mace", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, {type="armor", subtype="shield", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, {type="armor", subtype="heavy", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, - {type="jewelry", subtype="lite", defined="ELDRITCH_PEARL", autoreq=true}, + {type="jewelry", subtype="lite", defined="ELDRITCH_PEARL", autoreq=true, + replace_unique={type="lite", forbid_power_source={antimagic=true}, ignore_material_restriction=true}} }, resolvers.drops{chance=100, nb=1, {defined="SLASUL_NOTE"} }, resolvers.drops{chance=100, nb=5, {tome_drops="boss"} }, diff --git a/game/modules/tome/data/zones/thieves-tunnels/npcs.lua b/game/modules/tome/data/zones/thieves-tunnels/npcs.lua index 0f2949c1a7b008b3e86dd26e3ecd4dd23089d598..ef68a8be097ff6fd9abd2b0feb303a5cb4b59ca0 100644 --- a/game/modules/tome/data/zones/thieves-tunnels/npcs.lua +++ b/game/modules/tome/data/zones/thieves-tunnels/npcs.lua @@ -29,6 +29,7 @@ newEntity{ define_as = "ASSASSIN_LORD", cant_be_moved = true, resolvers.drops{chance=20, nb=1, {} }, + resolvers.auto_equip_filters("Shadowblade"), resolvers.equip{ {type="weapon", subtype="dagger", autoreq=true, force_drop=true, tome_drops="boss"}, {type="weapon", subtype="dagger", autoreq=true, force_drop=true, tome_drops="boss"}, diff --git a/game/modules/tome/data/zones/trollmire/npcs.lua b/game/modules/tome/data/zones/trollmire/npcs.lua index 0e6caf305db1bbb7e36b11f2162203d33ccbe89b..f3d8b5608af0fa63aac98386209b5535a8f38778 100644 --- a/game/modules/tome/data/zones/trollmire/npcs.lua +++ b/game/modules/tome/data/zones/trollmire/npcs.lua @@ -230,6 +230,7 @@ newEntity{ define_as = "ALUIN", move_others=true, body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1 }, + resolvers.auto_equip_filters("Sun Paladin"), resolvers.equip{ {type="weapon", subtype="waraxe", force_drop=true, tome_drops="boss", autoreq=true}, {type="armor", subtype="shield", defined="SANGUINE_SHIELD", random_art_replace={chance=65}, autoreq=true}, diff --git a/game/modules/tome/data/zones/vor-pride/npcs.lua b/game/modules/tome/data/zones/vor-pride/npcs.lua index 14ae33b8d7c159d47050399d0540ff006ee9f178..8780293a40a344a08bb63f6f992394b85fbb1993 100644 --- a/game/modules/tome/data/zones/vor-pride/npcs.lua +++ b/game/modules/tome/data/zones/vor-pride/npcs.lua @@ -48,7 +48,7 @@ newEntity{ base="BASE_NPC_ORC_VOR", define_as = "VOR", resolvers.inscriptions(4, "rune"), max_inscriptions = 5, - body = { INVEN = 10, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, TOOL=1 }, + body = { INVEN = 20, MAINHAND=1, OFFHAND=1, BODY=1, HEAD=1, TOOL=1 }, resolvers.equip{ {type="weapon", subtype="staff", force_drop=true, tome_drops="boss", forbid_power_source={antimagic=true}, autoreq=true}, diff --git a/game/modules/tome/dialogs/CharacterSheet.lua b/game/modules/tome/dialogs/CharacterSheet.lua index 45c61c5df942eccfebd23a6d8f6da57c08f82569..6083a5242ed1ec7fca98dd8de8c1826240c3c9c8 100644 --- a/game/modules/tome/dialogs/CharacterSheet.lua +++ b/game/modules/tome/dialogs/CharacterSheet.lua @@ -395,7 +395,7 @@ end -- show the inventory screen function _M:showInventory() - if self.actor.no_inventory_access or not self.actor.player then return end + if not config.settings.cheat and (self.actor.no_inventory_access or not self.actor.player) then return end local d local titleupdator = self.actor:getEncumberTitleUpdator("Inventory") local offset = self.actor.off_weapon_slots @@ -882,7 +882,7 @@ The amount of %s automatically gained or lost each turn.]]):format(res_def.name, aspeed = 1/actor:combatSpeed(mean) end if type == "psionic" then actor:attr("use_psi_combat", -1) end - return {obj=o, atk=atk, dmg=dmg, apr=apr, crit=crit, crit_power=crit_power, aspeed=aspeed, range=range, mspeed=mspeed, archery=archery, mean=mean, ammo=ammo, block=mean.block, talented=mean.talented} + return {obj=o, atk=atk, dmg=dmg, apr=apr, crit=crit, crit_power=crit_power or 0, aspeed=aspeed, range=range, mspeed=mspeed, archery=archery, mean=mean, ammo=ammo, block=mean.block, talented=mean.talented} end -- display the combat (comparison) stats for a combat slot @@ -912,6 +912,65 @@ The amount of %s automatically gained or lost each turn.]]):format(res_def.name, self:mouseTooltip(self.TOOLTIP_COMBAT_BLOCK, s:drawColorStringBlended(self.font, ("Block : #00ff00#%s"):format(text), self.w*.14, h, 255, 255, 255, true))-- h = h + self.font_h end h = h + self.font_h + +-- DGDGDGDG Display old damage with previously unscaled stats +if config.settings.cheat then + + --- Previous combatDamage function with unscaled stat bonuses + -- Calculate combat damage for a weapon (with an optional damage field for ranged) + -- Talent bonuses are always based on the base weapon + local function combatDamageOld(self, weapon, adddammod, damage) + weapon = weapon or self.combat or {} + local dammod = self:getDammod(damage or weapon) + local totstat = 0 + for stat, mod in pairs(dammod) do + totstat = totstat + self:getStat(stat) * mod + end + if adddammod then + for stat, mod in pairs(adddammod) do + totstat = totstat + self:getStat(stat) * mod + end + end + if self:knowTalent(self["T_FORM_AND_FUNCTION"]) then totstat = totstat + self:callTalent(self["T_FORM_AND_FUNCTION"], "getDamBoost", weapon) end + local talented_mod = 1 + self:combatTrainingPercentInc(weapon) + local power = self:combatDamagePower(damage or weapon) + return self:rescaleDamage(0.3*(self:combatPhysicalpower(nil, weapon) + totstat) * power * talented_mod) + end + + local p_old_combatDamage, atc_old_combatDamage = rawget(player, "combatDamage") + player.combatDamage = combatDamageOld + local combat = get_combat_stats(player, type, inven_id, item) + local combatc = {} + if actor_to_compare then + atc_old_combatDamage = actor_to_compare and rawget(actor_to_compare, "combatDamage") + actor_to_compare.combatDamage = combatDamageOld + combatc = get_combat_stats(actor_to_compare, type, inven_id, item) + end + local dm = {} + local dammod = player:getDammod(combat.ammo and combat.ammo.combat or combat.mean) +--table.set(game, "debug", "combat", combat) + for stat, i in pairs(dammod) do + local name = Stats.stats_def[stat].short_name:capitalize() + if player:knowTalent(player.T_STRENGTH_OF_PURPOSE) then + if name == "Str" then name = "Mag" end + end + if combat.talented == "knife" and player:knowTalent(player.T_LETHALITY) then + if name == "Str" then name = "Cun" end + end + dm[#dm+1] = ("%d%% %s"):format(i * 100, name) + end + text = compare_fields(player, actor_to_compare, + function(actor, ...) + return actor == actor_to_compare and combatc.dmg or combat.dmg + end, + "%3d", "%+.0f", 1, false, false, dam) + self:mouseTooltip("OLD DAMAGE (unscaled stat bonuses)", s:drawColorStringBlended(self.font, ("Old Damage : #00ff00#%s [%s]"):format(text, table.concatNice(dm, ", ")), w, h, 255, 255, 255, true)) + h = h + self.font_h + player.combatDamage = p_old_combatDamage + if actor_to_compare then actor_to_compare.combatDamage = atc_old_combatDamage end +end +-- DGDGDGDG end old damage display + text = compare_fields(player, actor_to_compare, function(actor, ...) return actor == actor_to_compare and combatc.apr or combat.apr end, "%3d", "%+.0f", 1, false, false, dam) self:mouseTooltip(self.TOOLTIP_COMBAT_APR, s:drawColorStringBlended(self.font, ("APR : #00ff00#%s"):format(text), w, h, 255, 255, 255, true)) h = h + self.font_h text = compare_fields(player, actor_to_compare, function(actor, ...) return actor == actor_to_compare and combatc.crit or combat.crit end, "%3d%%", "%+.0f%%", 1, false, false, dam) diff --git a/game/modules/tome/dialogs/DownloadCharball.lua b/game/modules/tome/dialogs/DownloadCharball.lua deleted file mode 100644 index 99932141acfc0ad7fb9f83d25d4f1d396a9fc319..0000000000000000000000000000000000000000 --- a/game/modules/tome/dialogs/DownloadCharball.lua +++ /dev/null @@ -1,99 +0,0 @@ --- ToME - Tales of Maj'Eyal --- Copyright (C) 2009 - 2017 Nicolas Casalini --- --- This program is free software: you can redistribute it and/or modify --- it under the terms of the GNU General Public License as published by --- the Free Software Foundation, either version 3 of the License, or --- (at your option) any later version. --- --- This program is distributed in the hope that it will be useful, --- but WITHOUT ANY WARRANTY; without even the implied warranty of --- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --- GNU General Public License for more details. --- --- You should have received a copy of the GNU General Public License --- along with this program. If not, see <http://www.gnu.org/licenses/>. --- --- Nicolas Casalini "DarkGod" --- darkgod@te4.org - -require "engine.class" -local Dialog = require "engine.ui.Dialog" -local ListColumns = require "engine.ui.ListColumns" -local TextzoneList = require "engine.ui.TextzoneList" -local Separator = require "engine.ui.Separator" -local Image = require "engine.ui.Image" - -module(..., package.seeall, class.inherit(Dialog)) - -function _M:init() - Dialog.init(self, "Download charball", game.w * 0.8, game.h * 0.8) - - self:generateList() - - self.c_list = ListColumns.new{width=math.floor(self.iw - 10), height=self.ih - 10, scrollbar=true, sortable=true, columns={ - {name="Player", width=30, display_prop="player", sort="player"}, - {name="Character", width=70, display_prop="character", sort="character"}, - }, list=self.list, fct=function(item) self:importCharball(item) end, select=function(item, sel) self:select(item) end} - - self:loadUI{ - {left=0, top=0, ui=self.c_list}, - } - self:setFocus(self.c_list) - self:setupUI() - self:select(self.list[1]) - - self.key:addBinds{ - EXIT = function() game:unregisterDialog(self) end, - } -end - -function _M:generateList() - profile.chat:selectChannel("tome") - - -- Makes up the list - local list = {} - for login, user in pairs(profile.chat.channels.tome.users) do - if user.valid == "validate" and user.current_char_data and user.current_char_data.uuid then - list[#list+1] = { player=user.name, character=user.current_char, id=user.id, uuid=user.current_char_data.uuid } - end - end - -- Add known artifacts - table.sort(list, function(a, b) return a.character < b.character end) - self.list = list -end - -function _M:select(item) - if item then - end -end - -function _M:importCharball(item) - if not item or not item.uuid then return end - - local data = profile:getCharball(item.id, item.uuid) - local f = fs.open("/charballs/__import.charball", "w") - f:write(data) - f:close() - - savefile_pipe:ignoreSaveToken(true) - local ep = savefile_pipe:doLoad("__import", "entity", "engine.CharacterBallSave", "__import") - savefile_pipe:ignoreSaveToken(false) - for a, _ in pairs(ep.members) do - if a.__CLASSNAME == "mod.class.Player" then - mod.class.NPC.castAs(a) - engine.interface.ActorAI.init(a, a) - a.quests = {} - a.ai = "tactical" - a.ai_state = {talent_in=1} - a.no_drops = true - a.keep_inven_on_death = false - a.energy.value = 0 - a.player = nil - a.faction = "enemies" - game.zone:addEntity(game.level, a, "actor", game.player.x, game.player.y-1) - - game:unregisterDialog(self) - end - end -end diff --git a/game/modules/tome/dialogs/debug/AdvanceActor.lua b/game/modules/tome/dialogs/debug/AdvanceActor.lua index e3325807a6173fb68a1f3a877df8095bb26b4e07..8ef08813565907f23c29378d6a3aee3ad7ce941a 100644 --- a/game/modules/tome/dialogs/debug/AdvanceActor.lua +++ b/game/modules/tome/dialogs/debug/AdvanceActor.lua @@ -37,16 +37,35 @@ function _M:init(data) Dialog.init(self, ("DEBUG -- Levelup Actor: [%s] %s"):format(self.actor.uid, self.actor.name), 800, 500) - self.inputs = {} + -- set default inputs + self.inputs = {do_level_up=true, levelup=50, set_base_stats=true, set_bonus_stats=true} + self.c_tut = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, text=[[Levelup an actor. Optionally set Stat levels, learn all talents possible, and gain points to spend on Levelup. The actor is backed up before changes are made. (Use the "Restore" button to recover.) ]]} local top = self.c_tut.h + 10 - local lev_box = Numberbox.new{title="Advance to level: ", number=self.actor.level or 50, max=1000, min=1, chars=10, fct=function(value) + local do_level_up = Checkbox.new{title="", text="Text", default=self.inputs.do_level_up, check_last=false, + fct=function(checked) + end, + on_change=function(checked) + self.inputs.do_level_up = checked + if not checked then + self.lev_box:setText(tostring(self.actor.level)) + end + end + } + self.do_level_up = do_level_up + + local lev_box = Numberbox.new{title=" Advance to Level: ", number=self.inputs.levelup, max=1000, min=1, chars=6, fct=function(value) self.inputs.levelup = value self:finish() + end, + on_change = function(value) + self.inputs.levelup = value + self.base_stat_box:autovalue(self) + self.bonus_stat_box:autovalue(self) end } self.lev_box = lev_box @@ -63,15 +82,13 @@ The actor is backed up before changes are made. (Use the "Restore" button to re local restore = Button.new{text=rest_text, alpha_unfocus=rest_alpha, fct=function() local last_actor = _M.last_actors[self.actor] - if last_actor and #last_actor > 0 then local uid = self.actor.uid local la = table.remove(last_actor, #last_actor) game.log("#LIGHT_BLUE#Restoring [%s]%s from backup version %d", self.actor.uid, la.name, #last_actor+1) self.actor:replaceWith(la) self.actor.uid = uid - self.lev_box.number = self.actor.level - self.lev_box:updateText(0) + self.lev_box:setText(tostring(self.actor.level)) end self.restore.text, self.restore.alpha_unfocus = restore_text(self) self.restore:generate() -- force redraw @@ -80,7 +97,7 @@ The actor is backed up before changes are made. (Use the "Restore" button to re } self.restore = restore - local get_points = Checkbox.new{title="Set unlimited respec and gain points for stats, talents, and prodigies ", text="Text", default=false, check_last=false, + local get_points = Checkbox.new{title="Gain points for stats, talents, and prodigies (unlimited respec)", text="Text", default=false, check_last=false, fct=function(checked) end, on_change=function(checked) @@ -89,30 +106,56 @@ The actor is backed up before changes are made. (Use the "Restore" button to re } self.get_points = get_points - local set_statlvl = Checkbox.new{title=" Change Stats ", default=false, check_last=true, + local set_base_stats = Checkbox.new{title="", default=self.inputs.set_base_stats, check_last=false, fct=function(checked) - self.inputs.levelup_stats = checked + self.inputs.set_base_stats = checked end, on_change=function(checked) - self.inputs.levelup_stats = checked + self.inputs.set_base_stats = checked end } - self.set_statlvl = set_statlvl - local stat_box = Numberbox.new{title="Force all stats to: ", number="maximum for level", max=1000, min=1, chars=20, fct=function(value) - self.stat_box:updateText(0) - self.inputs.stat_levelup = value - self:finish() + self.set_base_stats = set_base_stats + + local base_stat_box = Numberbox.new{title=" Force all BASE stats to: ", number=self:autoStatLevel(self.inputs.levelup), max=1000, min=1, chars=6, fct=function(value) + self.inputs.base_stat_level = value end, on_change = function(value) - if not self.stat_box.inputted then value = 100 self.stat_box.inputted = true end - if not self.set_statlvl.checked then self.set_statlvl:select() end - self.stat_box.number = value - self.stat_box:updateText(0) - self.inputs.stat_levelup = value + if not self.set_base_stats.checked then self.set_base_stats:select() end + self.inputs.base_stat_level = value end } - self.stat_box = stat_box + base_stat_box.autovalue = function(self, dialog) + if dialog.inputs.set_base_stats then + self:setText(tostring(dialog:autoStatLevel(dialog.inputs.levelup))) + end + end + self.base_stat_box = base_stat_box + local set_bonus_stats = Checkbox.new{title="", default=self.inputs.set_bonus_stats, check_last=false, + fct=function(checked) + self.inputs.set_bonus_stats = checked + end, + on_change=function(checked) + self.inputs.set_bonus_stats = checked + end + } + self.set_bonus_stats = set_bonus_stats + + local bonus_stat_box = Numberbox.new{title=" Force all BONUS stats to: ", number=self:autoStatBonusLevel(self.inputs.levelup), max=1000, min=-50, chars=6, fct=function(value) + self.inputs.bonus_stat_level = value + end, + on_change = function(value) + if not self.set_bonus_stats.checked then self.set_bonus_stats:select() end + self.inputs.bonus_stat_level = value + end, + } + bonus_stat_box.autovalue = function(self, dialog) + if dialog.inputs.set_bonus_stats then + self:setText(tostring(dialog:autoStatBonusLevel(dialog.inputs.levelup))) + end + end + self.bonus_stat_box = bonus_stat_box + local set_tl = Checkbox.new{title="Learn Talents ", text="Text", default=false, check_last=true, fct=function(checked) end, @@ -177,19 +220,22 @@ The actor is backed up before changes are made. (Use the "Restore" button to re local ok = Button.new{text="Accept", fct=function() self:finish() end} local cancel = Button.new{text="Cancel", fct=function() self:cancelclick() end} - local top = lev_box.h + get_points.h + self.c_tut.h + 15 + local top = lev_box.h + self.c_tut.h + 15 self:loadUI{ {left=10, top=0, padding_h=10, ui=self.c_tut}, - {left=10, top=self.c_tut.h+5, padding_h=10, ui=lev_box}, - {left=10, top=self.c_tut.h+lev_box.h+10, padding_h=10, ui=get_points}, + {left=10, top=self.c_tut.h+5, padding_h=10, ui=do_level_up}, + {left=do_level_up.w+10, top=self.c_tut.h+5, padding_h=10, ui=lev_box}, + {left=do_level_up.w+lev_box.w+20, top=self.c_tut.h+5, padding_h=10, ui=get_points}, {right=10, top=0, padding_h=10, ui=restore}, - {left=10, top=top, padding_h=10, ui=set_statlvl}, - {left=set_statlvl.w+20, top=top, padding_h=10, ui=stat_box}, - {left=10, top=top+stat_box.h+5, padding_h=10, ui=set_tl}, - {left=set_tl.w+20, top=top+stat_box.h+5, padding_h=10, ui=tl_box}, - {right=10, top=top+stat_box.h+5, padding_h=10, ui=force_tl}, - {left=10, top=top+stat_box.h+tl_box.h+10, padding_h=10, ui=mastery_box}, - {right=10, top=top+stat_box.h+tl_box.h+10, padding_h=10, ui=learn_all_talents}, + {left=10, top=top, padding_h=10, ui=set_base_stats}, + {left=set_base_stats.w+10, top=top, padding_h=10, ui=base_stat_box}, + {left=10, top=top+set_base_stats.h+5 , padding_h=10, ui=set_bonus_stats}, + {left=set_bonus_stats.w+10, top=top+set_base_stats.h+5, padding_h=10, ui=bonus_stat_box}, + {left=10, top=top+base_stat_box.h+bonus_stat_box.h+10, padding_h=10, ui=set_tl}, + {left=set_tl.w+20, top=top+base_stat_box.h+bonus_stat_box.h+10, padding_h=10, ui=tl_box}, + {right=10, top=top+base_stat_box.h+bonus_stat_box.h+10, padding_h=10, ui=force_tl}, + {left=10, top=top+base_stat_box.h+bonus_stat_box.h+tl_box.h+15, padding_h=10, ui=mastery_box}, + {right=10, top=top+base_stat_box.h+bonus_stat_box.h+tl_box.h+15, padding_h=10, ui=learn_all_talents}, {left=10, bottom=0, ui=ok}, {right=10, bottom=0, ui=cancel}, } @@ -213,13 +259,23 @@ end -- Levelup the actor function _M:finish() - self.inputs.levelup = self.inputs.levelup or tonumber(self.lev_box.number) --- print("[ForceLevelUp] inputs:", self.inputs) table.print(self.inputs, '\t_inputs_') + self.inputs.levelup = self.inputs.do_level_up and (self.inputs.levelup or tonumber(self.lev_box.number)) or self.actor.level + print("[ForceLevelUp] inputs:", self.inputs) table.print(self.inputs, '\t_inputs_') + +-- debugging + local inpts={} + for i, v in pairs(self.inputs) do + inpts[#inpts+1] = ("%s = %s"):format(i, v) + end + game.log("#LIGHT_BLUE#AdvanceActor inputs: %s", table.concatNice(inpts, ", ")) +-- debugging game:unregisterDialog(self) + local data = table.clone(self.inputs) - data.stat_level = self.inputs.levelup_stats and (self.inputs.stat_levelup or self:autoStatLevel(self.inputs.levelup)) + data.base_stat_level = self.inputs.set_base_stats and (self.inputs.base_stat_level or self:autoStatLevel(self.inputs.levelup)) + data.bonus_stat_level = self.inputs.set_bonus_stats and (self.inputs.bonus_stat_level or self:autoStatBonusLevel(self.inputs.levelup)) data.talent_level = self.inputs.levelup_talents and (self.inputs.talent_levelup or self:autoTalentLevel(self.inputs.levelup)) - self:levelupActor(self.actor, self.inputs.levelup, data) + self:levelupActor(self.actor, self.inputs.do_level_up and self.inputs.levelup, data) end function _M:cancelclick() @@ -230,25 +286,32 @@ function _M:autoTalentLevel(charlev) return 5 + math.max(0, math.floor((charlev - 50) / 10)) end +--- returns maximum stat levels vs. character as allowed by dialogs.CharacterSheet.lua function _M:autoStatLevel(charlev) - return math.min(charlev*1.4 + 20, 60 + math.max(0, charlev - 50)) + return math.floor(math.min(charlev*1.4 + 20, 60 + math.max(0, charlev - 50))) +end + +--- returns estimated stat bonuses vs. character level )+40 @ level 50 with diminishing returns) +function _M:autoStatBonusLevel(charlev) + return math.round(math.min(40*((charlev-1)/50)^.5 + 1, charlev*0.8)) end ---- Levelup Actor, possibly increasing stats, and learning talents +--- Levelup Actor, possibly increasing stats and learning talents -- who = actor to level up -- lev = target character level -- data = table of optional levelup parameters: --- stat_level: adjust stat bonuses to force all primary stats to this value +-- base_stat_level: force all primary base stats to this value +-- bonus_stat_level: force all primary stats bonuses to this value -- talent_level: learn all possible talent to this (raw) level -- ignore_talent_limits: ignore restrictions when levelling up talents -- alltalents: unlock all talent types in the game (before learning talents) --- set_mastery: force mastery level for all talent types +-- set_mastery: force mastery level for all known talent types function _M:levelupActor(who, lev, data) who = who or game.player -- backup the character _M.last_actors[who] = _M.last_actors[who] or {} table.insert(_M.last_actors[who], who:cloneFull()) - game.log("#LIGHT_BLUE#Advancing actor %s[%s]", who.name, who.uid) +-- game.log("#LIGHT_BLUE#Advancing actor %s[%s]", who.name, who.uid) local tt_def=who.talents_types_def who.lastLearntTalentsMax = function(what) return 500 end @@ -259,20 +322,20 @@ function _M:levelupActor(who, lev, data) end lev = who.level - game.logPlayer(who, "#LIGHT_BLUE#Level %d: Setting primary stats to %s, maximum talent levels to %s", lev, data.stat_level, data.talent_level) - if data.stat_level then - game.logPlayer(who, "#GOLD#Forcing all Stats to %s", data.stat_level) + if data.base_stat_level then + game.log("%s #GOLD#Forcing all Base Stats to %s", who.name, data.base_stat_level) for stat = 1, 6 do - local inc = data.stat_level - who:getStat(stat) - who:incIncStat(stat, inc) + local inc = data.base_stat_level - who:getStat(stat, nil, nil, true) + who:incStat(stat, inc) end end - if data.set_mastery then game.logPlayer(who, "#GOLD#Resetting all talents_types_mastery to %s", data.set_mastery) end + + if data.set_mastery then game.log("%s #GOLD#Resetting all talents_types_mastery to %s", who.name, data.set_mastery) end if data.alltalents then - game.logPlayer(who, "#GOLD#Unlocking All Talent Types") + game.log("%s #GOLD#Unlocking All Talent Types", who.name) for key, value in ipairs(tt_def) do who:learnTalentType(tt_def[key].type) - game.logPlayer(who, "#LIGHT_BLUE#%s -- %s",key, value.type) + game.log("#LIGHT_BLUE#%s -- %s", key, value.type) end end for tt, _ in pairs(who.talents_types) do @@ -281,7 +344,7 @@ function _M:levelupActor(who, lev, data) who:setTalentTypeMastery(tt, data.set_mastery) end if ttd and data.talent_level then - game.logPlayer(who, "#GOLD#Checking %s Talents (%s)", tt, who:getTalentTypeMastery(tt)) + game.log("#GOLD#Checking %s Talents (%s)", tt, who:getTalentTypeMastery(tt)) who:learnTalentType(tt, true) for i, t in pairs(ttd.talents) do if not (t.is_object_use or t.is_inscription or t.uber) then @@ -294,12 +357,21 @@ function _M:levelupActor(who, lev, data) else break end end - if learned then game.logPlayer(who, "#LIGHT_BLUE#Talent %s learned to level %d", t.id, who:getTalentLevelRaw(t)) end + if learned then game.log("#LIGHT_BLUE#Talent %s learned to level %d", t.id, who:getTalentLevelRaw(t)) end end end end end end + + -- done last to reverse any passive stat bonuses from talents + if data.bonus_stat_level then + game.log("%s #GOLD#Forcing all Bonus Stats to %s", who.name, data.bonus_stat_level) + for stat = 1, 6 do + local inc = data.bonus_stat_level - (who:getStat(stat, nil, nil, false) - who:getStat(stat, nil, nil, true)) + who:incIncStat(stat, inc) + end + end if data.get_points then who:attr("infinite_respec", 1) game.state.birth.ignore_prodigies_special_reqs = true diff --git a/game/modules/tome/dialogs/debug/CreateItem.lua b/game/modules/tome/dialogs/debug/CreateItem.lua index 59ece7033baaaca35b4cb4e17e79aea89a4f3029..1e6ed7e67cfe1abf4d66f8dd34800e8f220779ad 100644 --- a/game/modules/tome/dialogs/debug/CreateItem.lua +++ b/game/modules/tome/dialogs/debug/CreateItem.lua @@ -29,6 +29,7 @@ local Object = require "mod.class.Object" module(..., package.seeall, class.inherit(Dialog, Focusable, UIGroup)) function _M:init() + self.actor = game.player self:generateList() Dialog.init(self, "DEBUG -- Create Object", 1, 1) self.list_width = 500 @@ -164,7 +165,7 @@ function _M:init() end end, } - -- game:onTickEnd(function() self:setFocus(self:getUIElement(3).ui, "mouse") end) + self:setFocus(self.o_list) end function _M:on_register() @@ -202,7 +203,7 @@ function _M:list_select(item, sel) local ok, ret, special = xpcall(function() item.obj = game.zone:finishEntity(game.level, "object", item.e) item.obj.identified = true - local od = item.obj:getDesc({do_color=true}, nil, true, game.player) + local od = item.obj:getDesc({do_color=true}, nil, true, self.actor) if type(od) == "string" then od = od:toTString() end table.insert(od, 1, true) table.insert(od, 1, "#CRIMSON#==Resolved Example==#LAST#") @@ -217,7 +218,74 @@ function _M:list_select(item, sel) end game:tooltipDisplayAtMap(game.w, game.h, item.desc or "") end - +end + +--- add object to targeted creature's inventory +function _M:objectToTarget(obj) + local p = game.player + game:unregisterDialog(self) + local tg = {type="hit", range=100, nolock=true, no_restrict=true, nowarning=true, no_start_scan=true, act_exclude={[p.uid]=true}} + local x, y, act + local co = coroutine.create(function() + x, y, act = p:getTarget(tg) + if x and y then + if act then + if act:getInven(act.INVEN_INVEN) then + if act:addObject(act.INVEN_INVEN, obj) then + game.zone:addEntity(game.level, obj, "object") + _M.findObject(self, obj, act) + else game.log("#LIGHT_BLUE#Could not add object to %s at (%d, %d)", act.name, x, y) + end + end + else + game.log("#LIGHT_BLUE#No creature to add object to at (%d, %d)", x, y) + end + end + game:registerDialog(self) + end) + coroutine.resume(co) +end + +--- Create the object, either dropping it or adding it to the player or npc's inventory +function _M:acceptObject(obj, actor) + if not obj then game.log("#LIGHT_BLUE#No object to create") + else + obj = obj:cloneFull() + actor = actor or self.actor or game.player + -- choose where to put object (default is current actor's inventory) + local d = Dialog:multiButtonPopup("Place Object", "Place the object where?", + {{name=("Inventory of %s%s"):format( actor.name, actor.player and " #LIGHT_GREEN#(player)#LAST#" or ""), choice="player", fct=function(sel) + if not obj.quest and not obj.plot then obj.__transmo = true end + actor:addObject(actor.INVEN_INVEN, obj) + game.zone:addEntity(game.level, obj, "object") + _M.findObject(self, obj, actor) + end}, + {name=("Drop @ (%s, %s)%s"):format(actor.x, actor.y, actor.player and " #LIGHT_GREEN#(player)#LAST#" or ""), choice="drop", fct=function(sel) + game.zone:addEntity(game.level, obj, "object", actor.x, actor.y) + game.log("#LIGHT_BLUE#Dropped %s at (%d, %d)", obj:getName({do_color=true}), actor.x, actor.y) + end}, + {name=("NPC Inventory"):format(), choice="npc", fct=function(sel) + local x, y, act = _M.objectToTarget(self, obj) + end}, + {name=("Cancel"):format(), choice="cancel", fct=function(sel) + end} + }, + nil, nil, -- autosize + function(sel) + if sel.fct then sel.fct(sel) end + end, + false, 4, 1 -- cancel on escape, default + ) + end +end + +--- Report all locations of obj in actor's inventories +function _M:findObject(obj, actor) + if not (obj and actor) then return end + local inv, slot, attached = actor:searchAllInventories(obj, function(o, who, inven, slot, attached) + game.log("#LIGHT_BLUE#OBJECT:#LAST# %s%s: #LIGHT_BLUE#[%s] %s {%s, slot %s} at (%s, %s)#LAST#", o:getName({do_color=true, no_add_name=true}), attached and (" (attached to: %s)"):format(attached:getName({do_color=true, no_add_name=true})) or "", who.uid, who.name, inven.name, slot, who.x, who.y) + end) + return inv, slot, attached end -- debugging: check for bad objects @@ -235,25 +303,23 @@ function _M:use(item) item:action() elseif item.unique then local n = not item.error and item.obj or game.zone:finishEntity(game.level, "object", item.e) - n:identify(true) - game.zone:addEntity(game.level, n, "object", game.player.x, game.player.y) - game.log("#LIGHT_BLUE#Created %s", n:getName({do_color=true})) - item.obj = nil + if n then + n:identify(true) + self:acceptObject(n) + end else local example = item.obj and not item.error game:registerDialog(GetQuantity.new("Number of items to make", "Enter 1-100"..(example and ", or 0 for the example item" or ""), 20, 100, function(qty) - game.log("#LIGHT_BLUE# Creating %d items:", qty) if qty == 0 and item.obj and not item.error then - game.zone:addEntity(game.level, item.obj, "object", game.player.x, game.player.y) - game.log("#LIGHT_BLUE#Created %s", item.obj:getName({do_color=true})) - item.obj = nil + self:acceptObject(item.obj) else + game.log("#LIGHT_BLUE# Creating %d items:", qty) Dialog:yesnoPopup("Ego", "Add an ego enhancement if possible?", function(ret) if not ret then for i = 1, qty do local n = game.zone:finishEntity(game.level, "object", item.e, {ego_chance=-1000}) n:identify(true) - game.zone:addEntity(game.level, n, "object", game.player.x, game.player.y) + game.zone:addEntity(game.level, n, "object", self.actor.x, self.actor.y) game.log("#LIGHT_BLUE#Created %s", n:getName({do_color=true})) end else @@ -267,7 +333,7 @@ function _M:use(item) for i = 1, qty do local n = game.zone:finishEntity(game.level, "object", item.e, f) n:identify(true) - game.zone:addEntity(game.level, n, "object", game.player.x, game.player.y) + game.zone:addEntity(game.level, n, "object", self.actor.x, self.actor.y) game.log("#LIGHT_BLUE#Created %s", n:getName({do_color=true})) end end) @@ -284,7 +350,7 @@ function _M:generateList(obj_list) obj_list = obj_list or game.zone.object_list self.raw_list = obj_list for i, e in ipairs(obj_list) do - if e.name and e.rarity and not (e.unique and self.uniques[e.unique]) then + if e.name and not (e.unique and self.uniques[e.unique]) then list[#list+1] = {name=e.name, unique=e.unique, e=e} if e.unique then self.uniques[e.unique] = true end end @@ -315,6 +381,12 @@ function _M:generateList(obj_list) end game.log("#LIGHT_BLUE#%d artifacts created.", count) end}) + table.insert(list, 1, {name = " #YELLOW#Random Object#LAST#", action=function(item) + game:unregisterDialog(self) + local d = require("mod.dialogs.debug.RandomObject").new() + game:registerDialog(d) + end} + ) local chars = {} for i, v in ipairs(list) do diff --git a/game/modules/tome/dialogs/debug/RandomActor.lua b/game/modules/tome/dialogs/debug/RandomActor.lua new file mode 100644 index 0000000000000000000000000000000000000000..c9fac6048a88a429416d5a8330ded6af7d8c6cc9 --- /dev/null +++ b/game/modules/tome/dialogs/debug/RandomActor.lua @@ -0,0 +1,398 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2017 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Dialog = require "engine.ui.Dialog" +local Textbox = require "engine.ui.Textbox" +local Button = require "engine.ui.Button" +local Textzone = require "engine.ui.Textzone" +local DebugConsole = require "engine.DebugConsole" +local SummonCreature = require "mod.dialogs.debug.SummonCreature" + +module(..., package.seeall, class.inherit(engine.ui.Dialog)) + +_M._default_base_filter = "" +_M._base_filter = _M._default_base_filter +_M._default_boss_data = "{}" +_M._boss_data = _M._default_boss_data + +--- formats function help for dialog output +local function formatHelp(f_lines, f_name, f_lnum) + local help = ("#LIGHT_GREEN#(From %s, line %s):#LAST#"):format(f_name or "unknown", f_lnum or "unknown") + for _, line in ipairs(f_lines) do + line = line:gsub("#", "_") + help = help.."\n "..line:gsub("\t", " ") + end + return help +end + +-- set up context sensitive help +local lines, fname, lnum = DebugConsole:functionHelp(game.zone.checkFilter) +_M.filter_help = "#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) +lines, fname, lnum = DebugConsole:functionHelp(game.state.entityFilterPost) +_M.filter_help = _M.filter_help.."\n#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) + +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) + +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 + + 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.) +(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} + self.dialog_txt = dialog_txt + tops[#tops+1]=tops[#tops] + dialog_txt.h + 5 + + local function base_text() + local txt + if _M._base_actor then + local r, rc = _M._base_actor:TextRank() + txt = (rc or "#WHITE#").._M._base_actor.name.."#LAST#" + else + txt = "#GREY#None#LAST#" + end + return ([[Current Base Actor: %s]]):format(txt) + end + local base_txt = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text = base_text(), can_focus=true} + base_txt.refresh = function(self, actor) + self.text = base_text() + self:generate() + end + base_txt.on_focus = function() _M.tooltip(_M._base_actor) _M.help_display = "filter_help" end + self.base_txt = base_txt + + local base_refresh = self.newButton{text="Generate", _actor_field="_base_actor", + fct=function() + self:generateBase() + game.log("#LIGHT_BLUE# Current base actor: %s", _M._base_actor and _M._base_actor.name or "none") + self.base_txt:refresh(_M._base_actor) + self.base_refresh.on_select() + end, + help_display = "filter_help" + } + self.base_refresh = base_refresh + tops[#tops+1]=tops[#tops] + base_refresh.h + 5 + + local base_make = self.newButton{text="Place", _actor_field="_base_actor", + fct=function() + self:placeActor(_M._base_actor) + end, + help_display = "filter_help" + } + self.base_make = base_make + + local base_reset_filter = self.newButton{text="Default Filter", _actor_field="_base_actor", + fct=function() + game.log("#LIGHT_BLUE# Reset base filter") + _M._base_filter = _M._default_base_filter + self.bf_box:setText(_M._base_filter) + end, + help_display = "filter_help" + } + self.base_reset_filter = base_reset_filter + + local base_clear = self.newButton{text="Clear", _actor_field="_base_actor", + fct=function() + game.log("#LIGHT_BLUE# Clear base actor: %s", _M._base_actor and _M._base_actor.name or "none") + if _M._base_actor then + _M._base_actor:removed() + _M._base_actor = nil + end + base_txt:refresh(_M._base_actor) + game.tooltip:erase() + end, + help_display = "filter_help" + } + self.base_clear = base_clear + + local bf_box = _M.newTextbox{title="#LIGHT_BLUE#Base Filter:#LAST# ", text=_M._base_filter or "{}", chars=120, max_len=500, + fct=function(text) + _M._base_filter = text + self:generateBase() + self.base_txt:refresh(_M._base_actor) + self.base_refresh.on_select() + end, + on_change=function(text) + _M._base_filter = text + end, + help_display = "filter_help" + } + self.bf_box = bf_box + tops[#tops+1]=tops[#tops] + bf_box.h + 10 + + local boss_info = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=[[The #ORANGE#Boss Data#LAST# is used to transform the base actor into a random boss (which will use a random actor if needed).]]} + self.boss_info = boss_info + tops[#tops+1]=tops[#tops] + boss_info.h + 5 + + local function boss_text() + local txt + if _M._boss_actor then + local r, rc = _M._boss_actor:TextRank() + txt = (rc or "#ORANGE#").._M._boss_actor.name.."#LAST#" + else + txt = "#GREY#None#LAST#" + end + return ([[Current Boss Actor: %s]]):format(txt) + end + local boss_txt = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=boss_text(), can_focus=true} + boss_txt.refresh = function(self) + self.text = boss_text() + self:generate() + end + boss_txt.on_focus = function() _M.tooltip(_M._boss_actor) _M.help_display = "data_help" end + self.boss_txt = boss_txt + + local boss_refresh = self.newButton{text="Generate", _actor_field="_boss_actor", + fct=function() + self:generateBoss() + boss_txt:refresh() + self.boss_refresh.on_select() + end, + help_display = "data_help" + } + self.boss_refresh = boss_refresh + tops[#tops+1]=tops[#tops] + boss_refresh.h + 5 + + local boss_reset_data = self.newButton{text="Default Data", _actor_field="_boss_actor", + fct=function() + game.log("#LIGHT_BLUE# Reset Randboss Data") + _M._boss_data = _M._default_boss_data + self.boss_data_box:setText(_M._boss_data) + end, + help_display = "data_help" + } + self.boss_reset_data = boss_reset_data + + local boss_make = self.newButton{text="Place", _actor_field="_boss_actor", + fct=function() + boss_txt:refresh() + self:placeActor(_M._boss_actor) + end, + help_display = "data_help" + } + self.boss_make = boss_make + + local boss_data_box = _M.newTextbox{title="#ORANGE#Boss Data:#LAST# ", text=_M._boss_data or "{}", chars=120, max_len=500, + fct=function(text) + _M._boss_data = text + self:generateBoss() + boss_txt:refresh() + self.boss_refresh.on_select() + end, + on_change=function(text) + _M._boss_data = text + end, + help_display = "data_help" + } + self.boss_data_box = boss_data_box + tops[#tops+1]=tops[#tops] + boss_data_box.h + 5 + + self:loadUI{ + {left=0, top=tops[1], ui=dialog_txt}, + {left=0, top=tops[2], ui=base_refresh}, + {left=base_refresh.w+10, top=tops[2], ui=base_make}, + {left=base_refresh.w+base_make.w+15, top=tops[2], ui=base_reset_filter}, + {left=base_refresh.w+base_make.w+base_reset_filter.w+20, top=tops[2], ui=base_clear}, + {left=base_refresh.w+base_make.w+base_reset_filter.w+base_clear.w+25, top=tops[2]+(base_refresh.h-base_txt.h)/2, ui=base_txt}, + {left=0, top=tops[3], ui=bf_box}, + {left=0, top=tops[4], ui=boss_info}, + {left=0, top=tops[5], ui=boss_refresh}, + {left=boss_refresh.w+10, top=tops[5], ui=boss_make}, + {left=boss_refresh.w+boss_make.w+15, top=tops[5], ui=boss_reset_data}, + {left=boss_refresh.w+boss_reset_data.w+boss_make.w+20, top=tops[5]+(boss_make.h-boss_txt.h)/2, ui=boss_txt}, + {left=0, top=tops[6], ui=boss_data_box}, + } + + self:setFocus(base_make) + self:setupUI(true, true) + self.key:addBinds{ EXIT = function() + game:unregisterDialog(self) + end, + LUA_CONSOLE = function() + if config.settings.cheat then + game:registerDialog(DebugConsole.new()) + end + end,} + self.key:addCommands{ + _F1 = function() -- Help for filters and data (at upper left) + local d = Dialog:simpleLongPopup("Filter and Data Help", +([[%s]]):format(_M[_M.help_display] or _M.filter_help), math.max(500, game.w/2) + ) + engine.Dialog.resize(d, d.w, d.h, 25, 25) + end, + } +end + +-- display the tooltip for an actor +function _M.tooltip(act) + if act then + local plr = game.player + game:tooltipDisplayAtMap(game.w, game.h, act:tooltip(plr.x, plr.y, plr), nil, true) + else + game:tooltipDisplayAtMap(game.w, game.h, "#GREY#No Actor to Display#LAST#", nil, true) + end +end + +--- Generate a Textbox with some extra properties +_M.newTextbox = function(t) + local self = Textbox.new(t) + self.help_display = t.help_display + self.on_focus_change = function(status) _M.help_display = self.help_display end + return self +end + +--- Generate a Button with some additional properties +_M.newButton = function(t) + local self = Button.new(t) + self._actor_field = t._actor_field or "_base_actor" + self.help_display = t.help_display + self.on_select = function() + _M.help_display = self.help_display + _M.tooltip(_M[self._actor_field]) + end + self.key:addCommands{ + _c = function() -- open character sheet + local act = _M[self._actor_field] + if act then + game.log("#LIGHT_BLUE#Inspect [%s]%s", act.uid, act.name) + game:registerDialog(require("mod.dialogs.CharacterSheet").new(act)) + else + game.log("#LIGHT_BLUE#No actor to inspect") + end + end, + _l = function() -- lua inspect actor + if core.key.modState("ctrl") then + game.key:triggerVirtual("LUA_CONSOLE") + return + end + local act = _M[self._actor_field] + if act then + game.log("#LIGHT_BLUE#Lua Inspect [%s]%s", act.uid, act.name) + local DebugConsole = require"engine.DebugConsole" + local d = DebugConsole.new() + game:registerDialog(d) + DebugConsole.line = "=__uids["..act.uid.."]" + DebugConsole.line_pos = #DebugConsole.line + d.changed = true + else + game.log("#LIGHT_BLUE#No actor to Lua inspect") + end + end, + } + return self +end + +-- generate base actor +function _M:generateBase() + local ok, filter + local fx = loadstring("return "..tostring(_M._base_filter)) + self.base_filter_function = fx + if fx then + setfenv(fx, _G) + ok, filter = pcall(fx) + end + if not ok or filter and type(filter) ~= "table" then + game.log("#LIGHT_BLUE#Bad filter for base actor: %s", _M._base_filter) + return + end + local m + ok, m = pcall(game.zone.makeEntity, game.zone, game.level, "actor", filter) + + if ok then + if m then + if _M._base_actor then _M._base_actor:removed() end + local plr = game.player + m = SummonCreature.finishActor(self, m, plr.x, plr.y) + _M._base_actor = m + else + game.log("#LIGHT_BLUE#Could not generate a base actor with filter: %s", _M._base_filter) + end + else + print("[DEBUG:RandomActor] actor creation error:", m) + game.log("#LIGHT_BLUE#Base actor could not be generated with filter [%s].\n Error:%s", _M._base_filter, m) + end +end + +-- generate random boss +function _M:generateBoss() + local base = _M._base_actor + if not base then + print("[DEBUG:RandomActor] generating random base actor") + base = game.zone:makeEntity(game.level, "actor") + end + + if base then + local ok, data + local fx = loadstring("return "..tostring(_M._boss_data)) + self.boss_data_function = fx + if fx then + setfenv(fx, _G) + ok, data = pcall(fx) + end + if not ok or data and type(data) ~= "table" then + game.log("#LIGHT_BLUE#Bad data for random boss actor: %s", _M._boss_data) + return + end + + local m + ok, m = pcall(game.state.createRandomBoss, game.state, base, data) + if ok then + if m then + m._debug_finished = false + if _M._boss_actor then _M._boss_actor:removed() end + local plr = game.player + m = SummonCreature.finishActor(self, m, plr.x, plr.y) + _M._boss_actor = m + else + game.log("#LIGHT_BLUE#Could not generate a base actor with data: %s", _M._boss_data) + end + else + print("[DEBUG:RandomActor] Random Boss creation error:", m) + game.log("#LIGHT_BLUE#ERROR: Random Boss could not be generated with data [%s].\n Error:%s", _M._boss_data, m) + end + end +end + +--- Place the generated actor +function _M:placeActor(actor) + if actor then + local place_actor = actor:cloneFull() + SummonCreature.placeCreature(self, place_actor) + end +end + +function _M:on_register() + game:onTickEnd(function() self.key:unicodeInput(true) end) +end + diff --git a/game/modules/tome/dialogs/debug/RandomObject.lua b/game/modules/tome/dialogs/debug/RandomObject.lua new file mode 100644 index 0000000000000000000000000000000000000000..8ed6ce392409ed08f85ef32dbec27539ce086112 --- /dev/null +++ b/game/modules/tome/dialogs/debug/RandomObject.lua @@ -0,0 +1,739 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2017 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +require "engine.ui.Dialog" +local List = require "engine.ui.List" +local GetQuantity = require "engine.dialogs.GetQuantity" +local Textbox = require "engine.ui.Textbox" +local Button = require "engine.ui.Button" +local Textzone = require "engine.ui.Textzone" +local Dropdown = require "engine.ui.Dropdown" +local Dialog = require "engine.ui.Dialog" +local DebugConsole = require "engine.DebugConsole" +local CreateItem = require "mod.dialogs.debug.CreateItem" + +module(..., package.seeall, class.inherit(engine.ui.Dialog)) + +_M._default_random_filter = "" +_M._random_filter = _M._default_random_filter + +-- default from game.state:generateRandart +_M._default_base_filter =[[{ignore_material_restriction=true, no_tome_drops=true, ego_filter={keep_egos=true, ego_chance=-1000}, special=function(e) return (not e.unique and e.randart_able) and (not e.material_level or e.material_level >= 2) and true or false end}]] +_M._base_filter = _M._default_base_filter +_M._default_randart_data = "{lev=resolvers.current_level}" +_M._randart_data = _M._default_randart_data + +--- formats function help for dialog output +local function formatHelp(f_lines, f_name, f_lnum) + local help = ("#LIGHT_GREEN#(From %-10.60s, line: %s):#LAST#"):format(f_name or "unknown", f_lnum or "unknown") + for _, line in ipairs(f_lines or {}) do + line = line:gsub("#", "_") + help = help.."\n "..line:gsub("\t", " ") + end + return help +end + +-- set up context sensitive help +local lines, fname, lnum = DebugConsole:functionHelp(game.zone.checkFilter) +_M.filter_help = "#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) +lines, fname, lnum = DebugConsole:functionHelp(game.state.entityFilter) +_M.filter_help = _M.filter_help.."\n#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) +lines, fname, lnum = DebugConsole:functionHelp(game.state.entityFilterAlter) +_M.filter_help = _M.filter_help.."\n#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) +lines, fname, lnum = DebugConsole:functionHelp(game.state.entityFilterPost) +_M.filter_help = _M.filter_help.."\n#GOLD#FILTER HELP#LAST# "..formatHelp(lines, fname, lnum) + +lines, fname, lnum = DebugConsole:functionHelp(game.state.generateRandart) +_M.data_help = "#GOLD#RANDART DATA HELP#LAST# "..formatHelp(lines, fname, lnum) + +lines, fname, lnum = DebugConsole:functionHelp(resolvers.resolveObject) +_M.resolver_genObj_help = "#GOLD#resolvers.resolveObject#LAST# "..formatHelp(lines, fname, lnum) + +--- configure resolvers that can be used +_M.resolver_choices = {{name="None", resolver=nil, desc="Don't apply a resolver"}, + {name="Equipment", resolver="equip", desc="Object will be equipped if possible, otherwise added to main inventory", + generate=function(dialog) -- generate an object, forcing antimagic check + local res_input, ok, t + if dialog._random_filter then + ok, t = dialog:interpretTable(dialog._random_filter, "equip resolver filter") + if not ok then return end + res_input = t + end + res_input = res_input or {} + res_input.check_antimagic = true + _M._random_filter_table = res_input + return resolvers.resolveObject(_M.actor, res_input, false, 5) + end, + }, + {name="Inventory", resolver="inventory", desc="Object added to main inventory"}, + {name="Drops", resolver="drops", desc="Object added to main inventory (dropped on death)"}, + {name="Attach Tinker", resolver="attachtinker", desc="Tinker will be attached to a worn object"}, + {name="Drop Randart (auto data)", resolver="drop_randart", desc="Random Artifact (dropped on death) added to main inventory, uses the Base Object or Base Filter plus Randart Data as input", + generate=function(dialog) -- build input from Randart Data and the base object filter + local res_input, ok, t = {} + if dialog._randart_data then + ok, t = dialog:interpretTable(dialog._randart_data, "Randart Data") + if not ok then return end + res_input.data = t + end + if res_input.data and _M._base_object then + res_input.data.base = _M._base_object + else + ok, t = dialog:interpretTable(dialog._base_filter, "base object filter") + if not ok then return end + res_input.filter = t + end + res_input.no_add = true + _M._drop_randart_input = res_input + return resolvers.calc.drop_randart({res_input}, dialog.actor) + end, + accept=function(o, filter, dialog) + local res_input = table.clone(_M._drop_randart_input) or {} + res_input._use_object = o + res_input.no_add = nil + local res = resolvers.drop_randart(res_input) + return resolvers.calc.drop_randart(res, dialog.actor) + end + }, + {name="Drop Randart", resolver="drop_randart", desc="Random Artifact (dropped on death) added to main inventory", + generate=function(dialog) -- drop_randart using the random filter as its input + local res_input, ok, t + if dialog._random_filter then + ok, t = dialog:interpretTable(dialog._random_filter, "drop_randart data") + if not ok then return end + res_input = t + end + res_input = res_input or {} + res_input.no_add = true + _M._drop_randart_input = res_input + return resolvers.calc.drop_randart({res_input}, dialog.actor) + end, + accept=function(o, filter, dialog) + local res_input = table.clone(_M._drop_randart_input) or {} + res_input._use_object = o + res_input.no_add = nil + local res = resolvers.drop_randart(res_input) + return resolvers.calc.drop_randart(res, dialog.actor) + end + }, +} + +for i, item in ipairs(_M.resolver_choices) do + item.desc = "#SALMON#"..item.desc.."#LAST#\n" + item.resolver_num = i + if item.resolver and resolvers[item.resolver] then + local lines, fname, lnum = DebugConsole:functionHelp(resolvers[item.resolver]) + item.help = "resolver_help_"..item.resolver + _M[item.help] = "#GOLD#RESOLVER HELP#LAST#\n"..item.desc.."#GOLD#resolvers."..item.resolver.."#LAST# "..formatHelp(lines, fname, lnum).."\n".._M.resolver_genObj_help + end + item.help = item.help or "resolver_genObj_help" +end +_M.resolver_num = 1 -- default to no resolver + +-- object_list to use? (Not for now) + +function _M:init(actor) + _M.actor = actor or _M.actor or game.player + if not game.level:hasEntity(_M.actor) then _M.actor = game.player end + engine.ui.Dialog.init(self, "DEBUG -- Create Random Object", 1, 1) + + local tops={0} self.tops = tops + + local dialog_txt = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=([[Generate objects randomly subject to filters and create Random Artifacts. +Use "Generate" to create objects for preview and inspection. +Use "Add Object" to choose where to put the object and add it to the game. +(Mouse over controls for a preview of the generated object/working Actor. (Press #GOLD#'L'#LAST# to lua inspect.) +#SALMON#Resolvers#LAST# act on the working actor (default: player) to generate a SINGLE object. +They use the #LIGHT_GREEN#Random filter#LAST# as input unless noted otherwise and control object destination. +Filters are interpreted by ToME and engine entity/object generation functions (game.zone:checkFilter, etc.). +Interpretation of tables is within the _G environment (used by the Lua Console) using the current zone's #YELLOW_GREEN#object_list#LAST#. +Hotkeys: #GOLD#'F1'#LAST# :: context sensitive help, #GOLD#'C'#LAST# :: Working Character Sheet, #GOLD#'I'#LAST# :: Working Character Inventory. +]])} + + self.dialog_txt = dialog_txt + + local random_info = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=([[The #LIGHT_GREEN#Random Filter#LAST# controls random generation of a normal object.]]):format()} + self.random_info = random_info + tops[#tops+1]=tops[#tops] + dialog_txt.h + random_info.h + 10 + + if not _M._random_object then self:generateRandom() end + + local function object_text(prefix, obj) + local txt + if obj then + txt = obj:getName({do_color=true}) + else + txt = "#GREY#None#LAST#" + end + return ([[%s: %s]]):format(prefix or "Object", txt) + end + + local random_refresh = self.newButton{text="Generate", _object_field="_random_object", + fct=function() + self:generateRandom() + self.random_txt:refresh(_M._random_object) + self.random_refresh.on_select() + end, + help_display = "filter_help" + } + self.random_refresh = random_refresh + tops[#tops+1]=tops[#tops] + random_refresh.h + 5 + + local random_make = self.newButton{text="Add Object", _object_field="_random_object", + fct=function() + self:acceptObject(_M._random_object, true, _M._random_filter_table) + end, + help_display = "filter_help" + } + self.random_make = random_make + + local random_reset_filter = self.newButton{text="Default Filter", _object_field="_random_object", + fct=function() + _M._random_filter = _M._default_random_filter + self.rf_box:setText(_M._random_filter) + end, + help_display = "filter_help" + } + self.random_reset_filter = random_reset_filter + + local random_clear = self.newButton{text="Clear Object", _object_field="_random_object", + fct=function() + if _M._random_object then + _M._random_object = nil + end + self.random_txt:refresh(_M._random_object) + self.random_txt:generate() -- force redraw + game.tooltip:erase() + end, + help_display = "filter_help" + } + self.random_clear = random_clear + + local random_txt = Textzone.new{width=500, auto_height=true, height=random_refresh.h, no_color_bleed=true, font=self.font, + text=object_text("#LIGHT_GREEN#Random Object#LAST#", _M._random_object), can_focus=true} + random_txt.refresh = function(self, object) + self.text = object_text("#LIGHT_GREEN#Random Object#LAST#", object) + self:generate() + end + random_txt.on_focus = function() _M:tooltip(_M._random_object) _M.help_display = "filter_help" end + self.random_txt = random_txt + + local rf_box = _M.newTextbox{title="#LIGHT_GREEN#Random Filter:#LAST# ", text=_M._random_filter or "{}", chars=120, max_len=1000, + fct=function(text) + _M._random_filter = text + self:generateRandom() + self.random_refresh.on_select() + self.random_txt:refresh(_M._random_object) + end, + on_change=function(text) + _M._random_filter = text + end + } + self.rf_box = rf_box + tops[#tops+1]=tops[#tops] + rf_box.h + 10 + + local base_info = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=([[The #LIGHT_BLUE#Base Filter#LAST# is to generate a base object for building a Randart.]]):format()} + self.base_info = base_info + tops[#tops+1]=tops[#tops] + base_info.h + 5 + + + local base_txt = Textzone.new{width=500, auto_height=true, height=random_refresh.h, no_color_bleed=true, font=self.font, + text = object_text("#LIGHT_BLUE#Base Object#LAST#", _M._base_object), can_focus=true} + base_txt.refresh = function(self, object) + self.text = object_text("#LIGHT_BLUE#Base Object#LAST#", _M._base_object) + self:generate() + end + base_txt.on_focus = function() _M:tooltip(_M._base_object) _M.help_display = "filter_help" end + self.base_txt = base_txt + + local base_refresh = self.newButton{text="Generate", _object_field="_base_object", no_finish=true, + fct=function() + self:generateBase() + self.base_txt:refresh(_M._base_object) + self.base_refresh.on_select() + end, + help_display = "filter_help" + } + self.base_refresh = base_refresh + tops[#tops+1]=tops[#tops] + base_refresh.h + 5 + + local base_make = self.newButton{text="Add Object", _object_field="_base_object", + fct=function() + self:acceptObject(_M._base_object, false, _M._base_filter_table) + end, + help_display = "filter_help" + } + self.base_make = base_make + + local base_reset_filter = self.newButton{text="Default Filter", _object_field="_base_object", + fct=function() + _M._base_filter = _M._default_base_filter + self.bf_box:setText(_M._base_filter) + end, + help_display = "filter_help" + } + self.base_reset_filter = base_reset_filter + + local base_clear = self.newButton{text="Clear Object", _object_field="_base_object", + fct=function() + if _M._base_object then + _M._base_object = nil + end + self.base_txt:refresh(_M._base_object) + self.base_txt:generate() -- force redraw + game.tooltip:erase() + end, + help_display = "filter_help" + } + self.base_clear = base_clear + + local bf_box = _M.newTextbox{title="#LIGHT_BLUE#Base Filter:#LAST# ", text=_M._base_filter or "{}", chars=120, max_len=1000, + fct=function(text) + _M._base_filter = text + self:generateBase() + self.base_refresh:on_select() + self.base_txt:refresh(_M._base_object) + end, + on_change=function(text) + _M._base_filter = text + end, + help_display = "filter_help" + } + self.bf_box = bf_box + tops[#tops+1]=tops[#tops] + bf_box.h + 10 + + local resolver_info = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=([[#SALMON#Resolver selected:#LAST# ]]):format()} + resolver_info.on_focus = function(id, ui) + _M.help_display = "resolver_genObj_help" + game:tooltipDisplayAtMap(game.w, game.h, "An object resolver interprets additional filter fields to generate an object and determine where it will go.", nil, true) + end + self.resolver_info = resolver_info + + local resolver_sel = Dropdown.new{text="Dropdown text", width=200, nb_items=#_M.resolver_choices, + list = _M.resolver_choices, + fct=function(item) + _M.resolver_num = item.resolver_num + end, + on_select=function(item, sel) + _M.help_display = item.help + game:tooltipDisplayAtMap(game.w, game.h, item.desc or "No Tooltip", nil, true) + end, + } + resolver_sel.on_focus = function(r_sel, ui) + _M.help_display = "resolver_genObj_help" + local sel = r_sel.c_list and r_sel.c_list.sel + if sel then _M.help_display = r_sel.c_list.list[sel].help end + game:tooltipDisplayAtMap(game.w, game.h, "Use this selector to choose which resolver to use", nil, true) + end + self.resolver_sel = resolver_sel + + local randart_info = Textzone.new{auto_width=true, auto_height=true, no_color_bleed=true, font=self.font, + text=([[#ORANGE#Randart Data#LAST# contains parameters used to generate a Randart (interpreted by game.state:generateRandart). +The #LIGHT_BLUE#Base Object#LAST# will be used if possible.]]):format()} + self.randart_info = randart_info + tops[#tops+1]=tops[#tops] + randart_info.h + 5 + + local randart_refresh = self.newButton{text="Generate", _object_field="_randart", + fct=function() + self:generateRandart() + self.randart_txt:refresh(_M._randart) + self.randart_refresh.on_select() + end, + help_display = "data_help" + } + self.randart_refresh = randart_refresh + tops[#tops+1]=tops[#tops] + randart_refresh.h + 5 + + local randart_make = self.newButton{text="Add Object", _object_field="_randart", + fct=function() + self.randart_txt:refresh(_M._randart) + self:acceptObject(_M._randart) + end, + help_display = "data_help" + } + self.randart_make = randart_make + + local randart_reset_data = self.newButton{text="Default Data", _object_field="_randart", + fct=function() + _M._randart_data = _M._default_randart_data + self.randart_data_box:setText(_M._randart_data) + end, + help_display = "data_help" + } + self.randart_reset_data = randart_reset_data + + local randart_data_box = _M.newTextbox{title="#ORANGE#Randart Data:#LAST# ", text=_M._randart_data or "{}", chars=120, max_len=1000, + fct=function(text) + _M._randart_data = text + self:generateRandart() + self.randart_refresh.on_select() + self.randart_txt:refresh(_M._randart) + end, + on_change=function(text) + _M._randart_data = text + end, + help_display = "data_help" + } + self.randart_data_box = randart_data_box + tops[#tops+1]=tops[#tops] + randart_data_box.h + 5 + + local randart_txt = Textzone.new{width=600, auto_height=true, height=random_refresh.h, no_color_bleed=true, font=self.font, + text=object_text("#ORANGE#Randart#LAST#", _M._randart), can_focus=true} + randart_txt.refresh = function(self, object) + self.text = object_text("#ORANGE#Randart#LAST#", object) + self:generate() + end + randart_txt.on_focus = function() _M:tooltip(_M._randart) _M.help_display = "data_help" end + self.randart_txt = randart_txt + + local show_inventory = _M.newButton{text="Show #GOLD#I#LAST#nventory", _object_field="actor", + fct=function() self:showInventory() end, + } + self.show_inventory = show_inventory + + local show_character_sheet = _M.newButton{text="Show #GOLD#C#LAST#haracter Sheet", _object_field="actor", + fct=function() self:characterSheet() end, + } + self.show_character_sheet = show_character_sheet + + local set_actor = _M.newButton{text="Set working actor: "..("[%s] %s"):format(_M.actor.uid, _M.actor.name), _object_field="actor", + fct=function() self:setWorkingActor() end, + } + set_actor.update = function(ui) + ui.text = "Set working actor: "..("[%s] %s%s"):format(_M.actor.uid, _M.actor.name, _M.actor.player and " #LIGHT_GREEN#(player)#LAST#" or "") + ui:generate() + end + set_actor:update() + self.set_actor = set_actor + + self:loadUI{ + {left=0, top=tops[1], ui=dialog_txt}, + {left=0, top=tops[1]+dialog_txt.h+5, ui=random_info}, + + {left=rf_box.w-resolver_info.w-resolver_sel.w+5, top=tops[2]-resolver_info.h, ui=resolver_info}, + {left=rf_box.w-resolver_sel.w+10, top=tops[2]-resolver_sel.h, ui=resolver_sel}, + + {left=0, top=tops[2], ui=random_refresh}, + {left=random_refresh.w + 10, top=tops[2], ui=random_make}, + {left=random_refresh.w+random_make.w+15, top=tops[2], ui=random_reset_filter}, + {left=random_refresh.w+random_make.w+random_reset_filter.w+20, top=tops[2], ui=random_clear}, + {left=random_refresh.w+random_make.w+random_clear.w+random_reset_filter.w+25, top=tops[2]+(random_refresh.h-random_txt.h)/2, ui=random_txt}, + {left=0, top=tops[3], ui=rf_box}, + + {left=0, top=tops[4], ui=base_info}, + {left=0, top=tops[5], ui=base_refresh}, + {left=random_refresh.w + 10, top=tops[5], ui=base_make}, + {left=random_refresh.w+random_make.w+15, top=tops[5], ui=base_reset_filter}, + {left=random_refresh.w+random_make.w+base_reset_filter.w + 20, top=tops[5], ui=base_clear}, + {left=random_refresh.w+random_make.w+random_clear.w+base_reset_filter.w+25, top=tops[5]+(base_refresh.h-base_txt.h)/2, ui=base_txt}, + {left=0, top=tops[6], ui=bf_box}, + + {left=0, top=tops[7], ui=randart_info}, + {left=0, top=tops[8], ui=randart_refresh}, + {left=randart_refresh.w+10, top=tops[8], ui=randart_make}, + {left=randart_refresh.w+randart_make.w + 15, top=tops[8], ui=randart_reset_data}, + {left=randart_refresh.w+randart_make.w+randart_reset_data.w+20, top=tops[8]+(randart_refresh.h-randart_txt.h)/2, ui=randart_txt}, + {left=0, top=tops[9], ui=randart_data_box}, + {left=5, top=tops[10], ui=show_inventory}, + {left=show_inventory.w+10, top=tops[10], ui=show_character_sheet}, + {left=show_inventory.w+show_character_sheet.w+15, top=tops[10], ui=set_actor}, + } + + self:setupUI(true, true) + self.key:addBinds{ EXIT = function() + game:unregisterDialog(self) + end, + LUA_CONSOLE = function() + if config.settings.cheat then + game:registerDialog(DebugConsole.new()) + end + end,} + self.key:addCommands{ + _F1 = function() self:help() end, + __TEXTINPUT = function(c) + if (c == 'i' or c == 'I') then + self:showInventory() + end + if (c == 'c' or c == 'C') then + self:characterSheet() + end + end, + } + + if self.resolver_sel.c_list then + resolver_sel.c_list:select(_M.resolver_num or 1) + resolver_sel.c_list.key:addCommands{ + _F1 = function() -- help for resolver drop-down list + self:help() + end,} + end +end + +--- display the tooltip for an object or actor +function _M:tooltip(what) + if what then + if what.__CLASSNAME == "mod.class.Object" then + game:tooltipDisplayAtMap(game.w, game.h, what:getDesc({do_color=true}, nil, true, _M.actor), nil, true) + elseif what.__is_actor then + game:tooltipDisplayAtMap(game.w, game.h, what:tooltip(what.x, what.y, what), nil, true) + end + else + game:tooltipDisplayAtMap(game.w, game.h, "#GREY#No Tooltip to Display#LAST#", nil, true) + end +end + +--- Display context sensitive help (at upper left) +function _M:help() + local d = Dialog:simpleLongPopup("Filter/Data/Resolver Reference", +([[%s]]):format(_M[_M.help_display] or _M.filter_help), math.max(500, game.w/2) + ) + engine.Dialog.resize(d, d.w, d.h, 25, 25) +end + +--- Generate a Textbox with some extra properties +_M.newTextbox = function(t) + local self = Textbox.new(t) + self.help_display = t.help_display + self.on_focus_change = function(status) _M.help_display = self.help_display end + return self +end + +--- Generate a Button with some additional properties +_M.newButton = function(t) + local self = Button.new(t) + self.no_finish = t.no_finish + self._object_field = t._object_field or "_random_object" + self.help_display = t.help_display + self.on_select = function() + _M.help_display = self.help_display or _M.help_display + _M:tooltip(_M[self._object_field]) + self.key:addCommands{ + _l = function() -- lua inspect object + if core.key.modState("ctrl") then + game.key:triggerVirtual("LUA_CONSOLE") + return + end + local obj = _M[self._object_field] + if obj then + game.log("#LIGHT_BLUE#Lua Inspect [%s] %s", obj.uid, obj.name) + local DebugConsole = require"engine.DebugConsole" + local d = DebugConsole.new() + game:registerDialog(d) + DebugConsole.line = "=__uids["..obj.uid.."]" + DebugConsole.line_pos = #DebugConsole.line + d.changed = true + else + game.log("#LIGHT_BLUE#Nothing to Lua inspect") + end + end, + } + end + return self +end + +--- Generate an object using a filter +function _M:generateByFilter(filter) + local o = game.zone:makeEntity(game.level, "object", filter, nil, true) + return o +end + +--- translate text into a table +function _M:interpretTable(text, label) + local ok, t + local fx = loadstring("return "..tostring(text)) + if fx then + setfenv(fx, _G) + ok, t = pcall(fx) + end + if not ok or t and type(t) ~= "table" then + game.log("#LIGHT_BLUE#Bad %s: %s", label or "table definition", text) + return ok + end + return ok, t +end + +--- generate random object using the selected resolver if needed +function _M:generateRandom() + local ok, filter = self:interpretTable(_M._random_filter, "random object filter") + if not ok then return end + _M._random_filter_table = filter + local o + local apply_resolver = _M.resolver_num and _M.resolver_choices[_M.resolver_num] + local r_name = apply_resolver and apply_resolver.resolver + if r_name then -- generate the object using the resolver's base code, but don't add it to the game + game.log("#LIGHT_BLUE# Generate Random object using resolver: %s", r_name) + if apply_resolver.generate then + ok, o = pcall(apply_resolver.generate, self) + else + ok, o = pcall(resolvers.resolveObject, _M.actor, filter, false) + end + else + ok, o = pcall(_M.generateByFilter, self, filter) + end + if ok then + if o then + game.log("#LIGHT_BLUE# New random%s object: %s", r_name and (" (resolver: %s)"):format(r_name) or "", o:getName({do_color=true})) + _M._random_object = _M:finishObject(o) + else + game.log("#LIGHT_BLUE#Could not generate a random object with filter: %s", _M._random_filter) + end + else + print("[DEBUG:RandomObject] random object generation error:", o) + game.log("#LIGHT_BLUE#ERROR generating random object with filter [%s].\n Error: %s", _M._random_filter, o) + end +end + +--- generate base object for randart generation +function _M:generateBase() + local ok, filter = self:interpretTable(_M._base_filter, "base object filter") + if not ok then return end + _M._base_filter_table = filter + local o + ok, o = pcall(_M.generateByFilter, self, filter) + if ok then + if o then + _M._base_object = o + o:identify(true) + else + game.log("#LIGHT_BLUE#Could not generate a base object with filter: %s", _M._base_filter) + end + else + print("[DEBUG:RandomObject] Base object generation error:", o) + game.log("#LIGHT_BLUE#ERROR generating base object with filter [%s].\n Error:%s", _M._base_filter, o) + end +end + +--- generate Randart +function _M:generateRandart() + local ok, data = self:interpretTable(_M._randart_data, "Randart Data") + if not ok then return end + + local base = _M._base_object + data = data or {} + if base then data.base = data.base or base end + + local o + ok, o = pcall(game.state.generateRandart, game.state, data) + if ok then + if o then + o._debug_finished = false + _M._randart = _M:finishObject(o) + else + game.log("#LIGHT_BLUE#Could not generate a Randart with data: %s", _M._randart_data) + end + else + print("[DEBUG:RandomObject] Randart creation error:", o) + game.log("#LIGHT_BLUE#ERROR generating Randart with data [%s].\n Error:%s", _M._randart_data, o) + end +end + +-- finish generating the object (without adding it to the game) +function _M:finishObject(object) + if object and not object._debug_finished then + object._debug_finished = true + object = game.zone:finishEntity(game.level, "object", object) + object:identify(true) + end + return object +end + +function _M:on_register() + game:onTickEnd(function() self.key:unicodeInput(true) end) +end + +--- Add the object to the game, possibly using a resolver to do so +function _M:acceptObject(o, apply_resolver, filter) + if not o then game.log("#LIGHT_BLUE#No object to add") return end + apply_resolver = apply_resolver and _M.resolver_num and _M.resolver_choices[_M.resolver_num] + local r_name = apply_resolver and apply_resolver.resolver + if r_name then -- place the object according to the resolver + o = o:cloneFull() + local ok, ret + -- game.log("#LIGHT_BLUE#Accept object %s with resolver %s", o:getName({do_color=true, no_add_name=true}), r_name) + if apply_resolver.accept then + ok, ret = pcall(apply_resolver.accept, o, filter, self) + else + local res_input = table.clone(filter) or {} + res_input._use_object = o + local res = resolvers[r_name]({res_input}) + ok, ret = pcall(resolvers.calc[r_name], res, _M.actor) + end + if not ok then + print("[DEBUG:acceptObject] resolver error:", ret) + game.log("#LIGHT_BLUE#ERROR accepting object with resolver %s.\n Error:%s", r_name, ret) + return + end + -- report location of the object + CreateItem.findObject(self, o, _M.actor) + else -- select destination directly + CreateItem.acceptObject(self, o, _M.actor) + end +end + +--- Set the working actor (default for resolvers and object placement) +function _M:setWorkingActor() + local p = game.player + game:unregisterDialog(self) + local tg = {type="hit", range=100, nolock=true, no_restrict=true, nowarning=true, no_start_scan=true, act_exclude={[p.uid]=true}} + local x, y, act + local co = coroutine.create(function() + x, y, act = p:getTarget(tg) + if x and y then + if act then + game.log("#LIGHT_BLUE#Working Actor set to [%s]%s at (%d, %d)", act.uid, act.name, x, y) + _M.actor = act + self.set_actor:update() + end + end + game:registerDialog(self) + end) + coroutine.resume(co) +end + +--- show the inventory screen for the working actor +function _M:showInventory() + local d + local titleupdator = _M.actor:getEncumberTitleUpdator("Inventory") + d = require("mod.dialogs.ShowEquipInven").new(titleupdator(), _M.actor, nil, + function(o, inven, item, button, event) + if not o then return end + local ud = require("mod.dialogs.UseItemDialog").new(event == "button", _M.actor, o, item, inven, function(_, _, _, stop) + d:generate() + d:generateList() + d:updateTitle(titleupdator()) + if stop then game:unregisterDialog(d) -- game:unregisterDialog(self) + end + end) + game:registerDialog(ud) + end + ) + game:registerDialog(d) + d._actor_to_compare = d._actor_to_compare or game.player:clone() -- save comparison info +end + +--- Open character sheet for the working actor +function _M:characterSheet() + local d = require("mod.dialogs.CharacterSheet").new(_M.actor, "equipment") + game:registerDialog(d) +end \ No newline at end of file diff --git a/game/modules/tome/dialogs/debug/SummonCreature.lua b/game/modules/tome/dialogs/debug/SummonCreature.lua index 25270f85d4162b8b75029473545f89197455b09d..39381215896b34fcf5703e8496e298fda1e8d44c 100644 --- a/game/modules/tome/dialogs/debug/SummonCreature.lua +++ b/game/modules/tome/dialogs/debug/SummonCreature.lua @@ -33,6 +33,7 @@ function _M:init() self:loadUI{ {left=0, top=0, ui=list}, } + self:setupUI(true, true) self.key:addCommands{ __TEXTINPUT = function(c) @@ -60,17 +61,45 @@ end function _M:use(item) if not item then return end - game:unregisterDialog(self) if item.action then + game:unregisterDialog(self) item:action() else local m = game.zone:finishEntity(game.level, "actor", item.e) + + local plr = game.player + m = self:finishActor(m, plr.x, plr.y) self:placeCreature(m) end end +-- finish generating the actor (without adding it to the game) +function _M:finishActor(actor, x, y) + if actor and not actor._debug_finished then + actor._debug_finished = true + actor:resolveLevelTalents() -- make sure all talents have been learned + actor:resolve(); actor:resolve(nil, true) -- make sure all resolvers are complete + local old_escort = actor.make_escort -- making escorts fails without a position + actor.make_escort = nil + -- Note: this triggers functions "addedToLevel", "on_added", "on_added_to_level" which includes spawning escorts, updating for game difficulty, etc. + game.zone:addEntity(game.level, actor, "actor", nil, nil, true) + actor.make_escort = old_escort + -- remove all inventory items from unique list + if actor.inven then + for _, inven in pairs(actor.inven) do + for i, o in ipairs(inven) do + o:removed() + end + end + end + end + return actor +end + +--- Place the creature on the map function _M:placeCreature(m) + if not m then game.log("#LIGHT_BLUE# no actor to place.") return end local p = game.player local fx, fy = util.findFreeGrid(p.x, p.y, 20, true, {[engine.Map.ACTOR]=true}) if fx and fy then @@ -79,6 +108,7 @@ function _M:placeCreature(m) game.target.target.y = fy end + game:unregisterDialog(self) local tg = {type="hit", range=100, nolock=true, no_restrict=true, nowarning=true, no_start_scan=true, act_exclude={[p.uid]=true}} local x, y, act local co = coroutine.create(function() @@ -86,13 +116,21 @@ function _M:placeCreature(m) if x and y then if act then game.log("#LIGHT_BLUE#Actor [%s]%s already occupies (%d, %d)", act.uid, act.name, x, y) - return + else + game.zone:addEntity(game.level, m, "actor", x, y) + -- update uniques with inventory items + if m.inven then + for _, inven in pairs(m.inven) do + for i, o in ipairs(inven) do + o:added() + end + end + 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) end - game.zone:addEntity(game.level, m, "actor", x, y) - 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) - return x, y, act end + game:registerDialog(self) return end ) @@ -111,7 +149,11 @@ function _M:generateList() return a.name < b.name end) - table.insert(list, 1, {name = " Test Dummy", action=function(item) + table.insert(list, 1, {name = "#YELLOW#Random Actor#LAST#", action=function(item) + game:registerDialog(require("mod.dialogs.debug.RandomActor").new()) + end}) + + table.insert(list, 1, {name = "#PINK#Test Dummy#LAST#", action=function(item) local m = mod.class.NPC.new{define_as="TRAINING_DUMMY", type = "training", subtype = "dummy", name = "Test Dummy", color=colors.GREY, @@ -137,3 +179,4 @@ function _M:generateList() self.list = list end + diff --git a/game/modules/tome/init.lua b/game/modules/tome/init.lua index cf3cd115a8cd8c3200f7603d240ae2487c18d238..a4dd4110e08c01f3b6fc23b190ce9debc0c5d9fc 100644 --- a/game/modules/tome/init.lua +++ b/game/modules/tome/init.lua @@ -148,15 +148,15 @@ load_tips = { profile_defs = { allow_build = { {name="index:string:50"}, receive=function(data, save) save[data.name] = true end, export=function(env) for k, _ in pairs(env) do add{name=k} end end }, lore = { {name="index:string:30"}, receive=function(data, save) save.lore = save.lore or {} save.lore[data.name] = true end, export=function(env) for k, v in pairs(env.lore or {}) do add{name=k} end end }, - escorts = { {fate="index:enum(lost,betrayed,zigur,saved)"}, {nb="number"}, receive=function(data, save) inc_set(save, data.fate, data, "nb") end, export=function(env) for k, v in pairs(env) do add{fate=k, nb=v} end end }, - artifacts = { {cid="index:string:50"}, {name="index:string:40"}, {nb="number"}, receive=function(data, save) save.artifacts = save.artifacts or {} save.artifacts[data.cid] = save.artifacts[data.cid] or {} inc_set(save.artifacts[data.cid], data.name, data, "nb") end, export=function(env) for cid, d in pairs(env.artifacts or {}) do for name, v in pairs(d) do add{cid=cid, name=name, nb=v} end end end }, - characters = { {cid="index:string:50"}, {nb="number"}, receive=function(data, save) save.characters = save.characters or {} inc_set(save.characters, data.cid, data, "nb") end, export=function(env) for k, v in pairs(env.characters or {}) do add{cid=k, nb=v} end end }, - uniques = { {cid="index:string:50"}, {victim="index:string:50"}, {nb="number"}, receive=function(data, save) save.uniques = save.uniques or {} save.uniques[data.cid] = save.uniques[data.cid] or {} inc_set(save.uniques[data.cid], data.victim, data, "nb") end, export=function(env) for cid, d in pairs(env.uniques or {}) do for name, v in pairs(d) do add{cid=cid, victim=name, nb=v} end end end }, - deaths = { {cid="index:string:50"}, {source="index:string:50"}, {nb="number"}, receive=function(data, save) save.sources = save.sources or {} save.sources[data.cid] = save.sources[data.cid] or {} inc_set(save.sources[data.cid], data.source, data, "nb") end, export=function(env) for cid, d in pairs(env.sources or {}) do for name, v in pairs(d) do add{cid=cid, source=name, nb=v} end end end }, + escorts = { incr_only=true, {fate="index:enum(lost,betrayed,zigur,saved)"}, {nb="number"}, receive=function(data, save) inc_set(save, data.fate, data, "nb") end, export=function(env) for k, v in pairs(env) do add{fate=k, nb=v} end end }, + artifacts = { incr_only=true, {cid="index:string:50"}, {name="index:string:40"}, {nb="number"}, receive=function(data, save) save.artifacts = save.artifacts or {} save.artifacts[data.cid] = save.artifacts[data.cid] or {} inc_set(save.artifacts[data.cid], data.name, data, "nb") end, export=function(env) for cid, d in pairs(env.artifacts or {}) do for name, v in pairs(d) do add{cid=cid, name=name, nb=v} end end end }, + characters = { incr_only=true, {cid="index:string:50"}, {nb="number"}, receive=function(data, save) save.characters = save.characters or {} inc_set(save.characters, data.cid, data, "nb") end, export=function(env) for k, v in pairs(env.characters or {}) do add{cid=k, nb=v} end end }, + uniques = { incr_only=true, {cid="index:string:50"}, {victim="index:string:50"}, {nb="number"}, receive=function(data, save) save.uniques = save.uniques or {} save.uniques[data.cid] = save.uniques[data.cid] or {} inc_set(save.uniques[data.cid], data.victim, data, "nb") end, export=function(env) for cid, d in pairs(env.uniques or {}) do for name, v in pairs(d) do add{cid=cid, victim=name, nb=v} end end end }, + deaths = { incr_only=true, {cid="index:string:50"}, {source="index:string:50"}, {nb="number"}, receive=function(data, save) save.sources = save.sources or {} save.sources[data.cid] = save.sources[data.cid] or {} inc_set(save.sources[data.cid], data.source, data, "nb") end, export=function(env) for cid, d in pairs(env.sources or {}) do for name, v in pairs(d) do add{cid=cid, source=name, nb=v} end end end }, achievements = { {id="index:string:40"}, {gained_on="timestamp"}, {who="string:50"}, {turn="number"}, receive=function(data, save) save[data.id] = {who=data.who, when=data.gained_on, turn=data.turn} end, export=function(env) for id, v in pairs(env) do add{id=id, who=v.who, gained_on=v.when, turn=v.turn} end end }, donations = { no_sync=true, {last_ask="timestamp"}, receive=function(data, save) save.last_ask = data.last_ask end, export=function(env) add{last_ask=env.last_ask} end }, scores = { - nosync=true, + no_sync=true, receive=function(data,save) save.sc = save.sc or {} save.sc[data.world] = save.sc[data.world] or {} diff --git a/game/modules/tome/resolvers.lua b/game/modules/tome/resolvers.lua index 895ef42dc0a5f8a8daf65cbc0a4e8942834c5f2b..6255fb419e7540d09ee07ad8eb902082b05b095e 100644 --- a/game/modules/tome/resolvers.lua +++ b/game/modules/tome/resolvers.lua @@ -17,234 +17,430 @@ -- Nicolas Casalini "DarkGod" -- darkgod@te4.org +--local Birther = require "engine.Birther" local Talents = require "engine.interface.ActorTalents" ---- Resolves equipment creation for an actor -function resolvers.equip(t) - return {__resolver="equip", __resolve_last=true, t} -end -function resolvers.equipbirth(t) - for i, filter in ipairs(t) do - filter.ignore_material_restriction = true - end - return {__resolver="equip", __resolve_last=true, t} -end ---- Actually resolve the equipment creation -function resolvers.calc.equip(t, e) --- print("Equipment resolver for", e.name) - -- Iterate over object requests, try to create them and equip them - for i, filter in ipairs(t[1]) do --- print("Equipment resolver", e.name, filter.type, filter.subtype, filter.defined, filter.random_art_replace) - local o - local tries = 0 +--- General object resolver function (for Actors or other entities using the inventory interface) +-- creates a single object according to a filter, possibly equipping it +-- @param e: entity to resolve for (to which the object is added) +-- @param filter: filter to use when generating the object +-- @param do_wear: set true to wear the object if possible (Actors only), set false to not add the object to e +-- calls obj:wornLocations to find appropriate spots to wear +-- worn objects must be antimagic compatible with the wearing entity +-- @param tries: number of attempts allowed to generate the object, default 2 (5 if do_wear is set) +-- @return obj: the resolved object +-- Objects not worn are placed in main inventory (unless do_wear == false) +-- Objects are added to the game when resolved (affects uniques) +-- Additional filter fields interpreted by this resolver: +-- _use_object: object to use (skips random generation) +-- base_list: a specifier to load the entities list from a file, format: <classname>:<file path> +-- defined: specific name (matching obj.DEFINE_AS) for an object +-- replace_unique (requires defined): filter to replace specific object if it cannot be generated +-- set true to use (most) fields from the main filter +-- random_art_replace (requires defined): table of parameters for replacement object when dropping as loot +-- chance: chance to drop in place of the unique object (previously existing uniques are always replaced) +-- filter: object filter for replacement object (defaults to a unique, non-lore object) +-- alter: a function(obj, e) to modify the object (called after generation, before adding to e) +-- check_antimagic: force checking antimagic compatibility +-- autoreq: set true to force gaining talents, stats, levels to equip the object (requires do_wear) +-- force_inven: force adding the object to this inventory id (requires do_wear, skips normal checks) +-- force_item: force adding the object to this inventory slot (requires do_wear, force_inven) +-- force_drop: always drop the object on death (by default, only uniques are dropped) +-- never_drop: never drop the object on death +-- Additional filter fields are interpreted by other functions that can affect equipment generation: +-- @see engine.zone:checkFilter: type, subtype, name, define_as, unique, properties, not_properties, +-- check_filter, special, max_ood +-- @see game.state:entityFilterAlter: force_tome_drops, no_tome_drops, tome, tome_mod +-- @see game.state:entityFilter: ignore_material_restriction, tome_mod, forbid_power_source, power_source +-- @see game.state:entityFilterPost: random_object +-- @see game.state:egoFilter: automatically creates/updates the ego_filter field to check power_source compatibility +function resolvers.resolveObject(e, filter, do_wear, tries) + if do_wear then do_wear = e.__is_actor and do_wear end + filter = filter and table.clone(filter) or {} + tries = tries or do_wear and 5 or 2 + print("[resolveObject] CREATE FOR", e.uid, e.name, "do_wear/tries:", do_wear, tries, "filter:\n", (string.fromTable(filter, -1))) + local o = filter._use_object + if o then -- filter contains the object to use + print("[resolveObject] using pre-generated object", o.uid, o.name) + else + --print("[resolveObject]", e.name, "filter:", filter) table.print(filter, "\t") + local base_list + if filter.base_list then -- load the base list defined for makeEntityByName + local _, _, class, file = filter.base_list:find("(.*):(.*)") + if class and file then + base_list = require(class):loadList(file) + if base_list then + base_list.__real_type = "object" + else + print("[resolveObject] COULD NOT LOAD base_list:", filter.base_list) + end + end + end repeat local ok = true - tries = tries + 1 - if not filter.defined then - o = game.zone:makeEntity(game.level, "object", filter, nil, true) - else + tries = tries - 1 + if filter.defined then -- make a specific object local forced - o, forced = game.zone:makeEntityByName(game.level, "object", filter.defined, filter.random_art_replace and true or false) - -- If we forced the generation this means it was already found - if forced then --- print("Serving unique "..o.name.." but forcing replacement drop") - filter.random_art_replace.chance = 100 + o, forced = game.zone:makeEntityByName(game.level, base_list or "object", filter.defined, filter.random_art_replace and true or false) + if forced then -- If generation was forced, object is a previously existing unique + print("[resolveObject] FORCING UNIQUE (replaced on drop):", filter.defined, o.uid, o.name) + table.set(filter, "random_art_replace", "chance", 100) + elseif not o and filter.replace_unique then -- Replace with another object + local rpl_filter = filter.replace_unique + if type(rpl_filter) ~= "table" then + rpl_filter = table.clone(filter) + rpl_filter.ignore_material_restriction, rpl_filter.defined, rpl_filter.replace_unique = true, nil, nil + end + o = game.zone:makeEntity(game.level, base_list or "object", rpl_filter, nil, true) + if o then print("[resolveObject] REPLACING UNIQUE:", filter.defined, o.uid, o.name) + end end + if not o then break end + else -- make an object using the normal probabilities after applying the filter + o = game.zone:makeEntity(game.level, base_list or "object", filter, nil, true) end - if not game.state:checkPowers(e, o, nil, "antimagic_only") then + -- check for incompatible equipment + if (do_wear or filter.check_antimagic) and not game.state:checkPowers(e, o, nil, "antimagic_only") then ok = false - print(" Equipment resolver for ", e.name ," -- incompatible equipment ", o.name, "retrying", tries, "self.not_power_source:", e.not_power_source and table.concat(table.keys(e.not_power_source), ","), "filter forbid ps:", filter.forbid_power_source and table.concat(table.keys(filter.forbid_power_source), ","), "vs ps", o.power_source and table.concat(table.keys(o.power_source), ",")) - end - until ok or tries > 4 - if o then ---- print("Zone made us an equipment according to filter!", o:getName()) - -- curse (done here to ensure object attributes get applied correctly) - if e:knowTalent(e.T_DEFILING_TOUCH) then - local t = e:getTalentFromId(e.T_DEFILING_TOUCH) - t.curseItem(e, t, o) + print("[resolveObject] for ", e.uid, e.name ," -- incompatible equipment ", o.name, "retrying", tries, "self.not_power_source:", e.not_power_source and table.concat(table.keys(e.not_power_source), ","), "filter forbid ps:", filter.forbid_power_source and table.concat(table.keys(filter.forbid_power_source), ","), "vs ps", o.power_source and table.concat(table.keys(o.power_source), ",")) end + until o and ok or tries <= 0 + end + + if o then + print("[resolveObject] CREATED OBJECT:", o.uid, o.name, "tries left:", tries) + if filter.alter then filter.alter(o, e) end + -- curse (done here to ensure object attributes get applied correctly, good place for a talent callback?) + if do_wear ~= false and e.knowTalent and e:knowTalent(e.T_DEFILING_TOUCH) then + e:callTalent(e.T_DEFILING_TOUCH, "curseItem", o) + end - -- Auto alloc some stats to be able to wear it - if filter.autoreq and rawget(o, "require") and rawget(o, "require").stat then --- print("Autorequire stats") - for s, v in pairs(rawget(o, "require").stat) do - if e:getStat(s) < v then - e.unused_stats = e.unused_stats - (v - e:getStat(s)) - e:incStat(s, v - e:getStat(s)) + if do_wear then + if filter.autoreq then -- Auto alloc talents, stats, and levels to be able to wear the object + local req, oldreq = e:updateObjectRequirements(o) + if req then + if req.level and e.level < req.level then + print("[resolveObject] autoreq: LEVELUP to", req.level) + e:forceLevelup(req.level) + end + if req.talent then -- learn (forced) talents first (may affect stats) + for _, tid in ipairs(req.talent) do + local levls = 0 + if type(tid) == "table" then + levls = tid[2] - e:getTalentLevelRaw(tid[1]) + if levls > 0 then + print("[resolveObject] autoreq: LEARNING TALENT", tid[1], levls) + e:learnTalent(tid[1], true, levls) + end + else + if not e:knowTalent(tid) then + levls = 1 + print("[resolveObject] autoreq: LEARNING TALENT", tid) + e:learnTalent(tid, true, levls) + end + end + if levls > 0 then + local tal = e:getTalentFromId(tid[1]) + if tal and tal.generic then e.unused_generics = e.unused_generics - levls else e.unused_talents = e.unused_talents - levls end + end + end + end + if req.stat then + for s, v in pairs(req.stat) do + local gain = v - e:getStat(s) + if gain > 0 then + print("[resolveObject] autoreq: GAINING STAT", s, gain) + e.unused_stats = e.unused_stats - gain + e:incStat(s, gain) + end + end end end + o.require = oldreq end - if e:wearObject(o, true, false, filter.force_inven or nil, filter.force_item or nil) == false then - if filter.force_inven and e:getInven(filter.force_inven) then -- we just really want it - e:addObject(filter.force_inven, o, true, filter.force_item) - else - e:addObject(e.INVEN_INVEN, o) + + local worn, invens + -- select inventories where object may be worn + if filter.force_inven then + invens = e:getInven(filter.force_inven) + if invens then invens = {{inv=invens, slot=filter.force_item, force=true}} end + else + -- checks inventory equipment filters, sorts possible locations by object "power" + invens = o:wornLocations(e, nil, nil) + end + if invens then + for i, worn_inv in ipairs(invens) do + --print("[resolveObject] trying inventory", worn_inv.inv.name, worn_inv.slot, "for", o.uid, o.name, worn_inv.force and "FORCED" or "unforced") + worn = e:wearObject(o, true, false, worn_inv.inv, worn_inv.slot) + if worn == false then + if worn_inv.force then -- force adding the object + print("[resolveObject]", o.uid, o.name, "FORCING INVENTORY", worn_inv.inv.name, worn_inv.inv.id, "slot", worn_inv.slot) + local ro = e:removeObject(worn_inv.inv, worn_inv.slot or 1) + e:addObject(worn_inv.inv, o, true, worn_inv.slot) + if ro and e:addObject(e.INVEN_INVEN, ro) then -- replaced object to main inventory + print("\t\t moved replaced object to main inventory:", ro.uid, ro.name) + end + worn = true break + else + print("[resolveObject]", o.uid, o.name, "NOT WORN in", worn_inv.name, worn_inv.id) + end + else + print("[resolveObject]", o.uid, o.name, "added to inventory", worn_inv.inv.name) + --put a replaced object in main inventory + if type(worn) == "table" and e:addObject(e.INVEN_INVEN, worn) then + print("\t\t moved replaced object to main inventory:", worn.uid, worn.name) + end + break + end end end - -- Do not drop it unless it is an ego or better - if not o.unique then o.no_drop = true --[[print(" * "..o.name.." => no drop")]] end - if filter.force_drop then o.no_drop = nil end - if filter.never_drop then o.no_drop = true end - game.zone:addEntity(game.level, o, "object") + 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 + if not o.wielded then + print("[resolveObject] adding to main inventory:", o.uid, o.name) + e:addObject(e.INVEN_INVEN, o) + end + game.zone:addEntity(game.level, o, "object") -- updates uniques list to prevent duplicates + end - if t[1].id then o:identify(t[1].id) end + -- Set the object drop status (only drop uniques by default) + if not o.unique then o.no_drop = true end + if filter.force_drop then o.no_drop = false end + if filter.never_drop then o.no_drop = true end - if filter.random_art_replace then - o.__special_boss_drop = filter.random_art_replace - end + if filter.random_art_replace then + o.__special_boss_drop = filter.random_art_replace end + else + print("[resolveObject] **FAILED** for", e.uid, e.name, "filter:", (string.fromTable(filter, 2))) end - -- Delete the origin field - return nil + return o end - --- Resolves equipment creation for an actor -function resolvers.attachtinker(t) - return {__resolver="attachtinker", __resolve_last=true, t} +-- @param t a table of object filters (resolvers.resolveObject is called for each) +-- additional filter fields interpreted: +-- id: identify the object +-- Objects that cannot be equipped are added to main inventory instead +function resolvers.equip(t) + return {__resolver="equip", __resolve_last=true, t, _allow_random_boss=true} end -function resolvers.attachtinkerbirth(t) + +--- Resolves equipment creation for an actor at birth (ignores material restrictions) +function resolvers.equipbirth(t) for i, filter in ipairs(t) do filter.ignore_material_restriction = true end - return {__resolver="attachtinker", __resolve_last=true, t} + return {__resolver="equip", __resolve_last=true, t, _allow_random_boss=true} end ---- Actually resolve the equipment creation -function resolvers.calc.attachtinker(t, e) - print("Tinker resolver for", e.name) - -- Iterate of object requests, try to create them and equip them + +--- Actually create and equip objects +-- Creates the objects according to the filters and equips them if possible +-- @param t the resolver table created by resolvers.equip +-- @param e the entity (Actor) to add the equipment to +function resolvers.calc.equip(t, e) + -- Iterate over object filters, trying to create and equip each for i, filter in ipairs(t[1]) do - print("Tinker resolver", e.name, filter.type, filter.subtype, filter.defined) - local o - if not filter.defined then - o = game.zone:makeEntity(game.level, "object", filter, nil, true) - else - local forced - o, forced = game.zone:makeEntityByName(game.level, "object", filter.defined, filter.random_art_replace and true or false) - -- If we forced the generation this means it was already found - if forced then --- print("Serving unique "..o.name.." but forcing replacement drop") - filter.random_art_replace.chance = 100 - end - end + --print("[resolvers.equip]", e.uid, e.name, (string.fromTable(filter, 1))) + local o = resolvers.resolveObject(e, filter, true, 5) if o then - print("Zone made us an Tinker according to filter!", o:getName()) - -- Auto alloc some stats to be able to wear it - if filter.autoreq and rawget(o, "require") and rawget(o, "require").stat then --- print("Autorequire stats") - for s, v in pairs(rawget(o, "require").stat) do - if e:getStat(s) < v then - e.unused_stats = e.unused_stats - (v - e:getStat(s)) - e:incStat(s, v - e:getStat(s)) - end + o._resolver_type = t.__resolver + if t[1].id then o:identify(t[1].id) end + end + end + return nil -- Deletes the origin field +end + +--- Sets filters by inventory name controlling which objects may be automatically equipped by an entity (Actor) +-- Actors (NPCs) will not auto equip items that don't pass the filter (or removed any equipped items) +-- @see Object:wornLocations +-- @param[1] t: a table of filters indexed by inventory name, format: +-- {[inven_def.name1] = {equipment filter 1}, [inven_def.name2] = {equipment filter 2}, ...} +-- filter.ignore_material_restriction and filter.allow_uniques are set true if not defined +-- Use the e._equipping_entity variable set by Object:wornLocations for the equipping actor within filter special functions +-- @param[2] t: a string matching the name of a Birther subclass ("Rogue", "Bulwark", ...) +-- the autoequip filters for the subclass will be copied +-- @param readonly set true to prevent the inventory filters from being overwritten by later applications +function resolvers.auto_equip_filters(t, readonly) + return {__resolver="auto_equip_filters", __resolve_instant=true, t, readonly=readonly, _allow_random_boss=true} +end + +--- Resolves the auto-equip filters for an actor by inventory slot +--function resolvers.calc.auto_equip_filters(t, e, readonly) +function resolvers.calc.auto_equip_filters(t, e, readonly) + local filters = t[1] + if type(filters) == "string" then -- get subclass filters + local c_name, ok = filters + local cc = table.get(engine.Birther.birth_descriptor_def, "subclass", c_name, "copy") + if cc then + print("[resolvers.auto_equip_filters] using birth descriptor for subclass:", c_name) + for i, res in ipairs(cc) do + if type(res) == "table" and res.__resolver == "auto_equip_filters" then + res = table.clone(res, true) + res.readonly = t.readonly or res.readonly + resolvers.calc.auto_equip_filters(res, e) ok = true end end + end + if not ok then print("[resolvers.auto_equip_filters] NO BIRTH auto_equip_filter for subclass:", c_name) end + return + end + for inv, filter in pairs(filters) do + local inven = e:getInven(inv) + if inven then + if not inven.auto_equip_filter or not inven.auto_equip_filter.readonly then + local aef = table.clone(filter, true) + if aef.ignore_material_restriction == nil then aef.ignore_material_restriction = true end + if aef.allow_uniques == nil then aef.allow_uniques = true end + aef.readonly = t.readonly + inven.auto_equip_filter = aef + end + end + end +end + +--- Resolves tinkers and attaches them to appropriate worn objects if possible +-- @param t a table of object filters (resolvers.resolveObject is called for each) +-- additional filter fields interpreted: +-- keep_object: place the tinker in main inventory if not attached (default is to discard it) +-- id: identify the tinker +-- a tinker already attached to the worn object is automatically placed in main inventory +function resolvers.attachtinker(t) + return {__resolver="attachtinker", __resolve_last=true, t, keep_object=t.keep_object, _allow_random_boss=true} +end +--- As resolvers.attachtinker but ignores material restrictions +function resolvers.attachtinkerbirth(t) + for i, filter in ipairs(t) do + filter.ignore_material_restriction = true + end + return {__resolver="attachtinker", __resolve_last=true, t, _allow_random_boss=true} +end + +--- Actually create and attach the tinker +-- @param t the resolver table created by resolvers.attachtinker +-- @param e the entity (Actor) to add the tinker to +function resolvers.calc.attachtinker(t, e) + -- Iterate over object filters, trying to create and attach each + for i, filter in ipairs(t[1]) do + --print("[resolvers.attachtinker]", e.uid, e.name, (string.fromTable(filter, 1))) + local o = resolvers.resolveObject(e, filter, false, 5) + if o then + o._resolver_type = t.__resolver + --print("[resolvers.attachtinker] created tinker:", o.uid, o:getName()) local base_inven, base_item = e:findTinkerSpot(o) - if base_inven and base_item then - local base_o = base_inven[base_item] - e:doWearTinker(nil, nil, o, base_inven, base_item, base_o, true) + local base_o = base_inven and base_item and base_inven[base_item] + local ok + if base_o then + ok = e:doWearTinker(nil, nil, o, base_inven, base_item, base_o, true) if t[1].id then o:identify(t[1].id) end + print("[resolvers.attachtinker]", o.uid, o.name, ok and "ATTACHED:" or "FAILED TO ATTACH:", base_inven.name, base_item, base_o and base_o:getName()) + else + print("[resolvers.attachtinker]", o.uid, o.name, "No tinker attach spot", base_inven, base_item) + end + if not ok then + if (t.keep_object or filter.keep_object) and e:addObject(e.INVEN_INVEN, o) then + print(" --- added to main inventory") ok = true + end end + if ok then game.zone:addEntity(game.level, o, "object") end -- updates uniques list to prevent duplicates end end - -- Delete the origin field - return nil + return nil -- Deletes the origin field end --- Resolves inventory creation for an actor +-- Similar to resolvers.equip, but places each object in a specific inventory slot +-- No checks are made for wearability (worn objects will not be replaced) +-- @param t a table of object filters (resolvers.resolveObject is called for each) +-- additional filter fields interpreted: +-- inven: inventory id of the inventory to add to (defaults to t.inven or main inventory) +-- keep_object: set true to try main inventory if the object cannot be added to the specified inventory +-- id: identify the object (defaults to t.id) +-- transmo: set true to designate the object for transmutation (defaults to t.transmo or nil) function resolvers.inventory(t) - return {__resolver="inventory", __resolve_last=true, t} + return {__resolver="inventory", __resolve_last=true, t, _allow_random_boss=true} end + +--- Resolves inventory creation for an actor at birth (ignores material restrictions) function resolvers.inventorybirth(t) for i, filter in ipairs(t) do filter.ignore_material_restriction = true end - return {__resolver="inventory", __resolve_last=true, t} + return {__resolver="inventory", __resolve_last=true, t, _allow_random_boss=true} end + --- Actually resolve the inventory creation +-- @param t the resolver table created by resolvers.inventory +-- @param e the entity (Actor) to add the objects to function resolvers.calc.inventory(t, e) - -- Iterate of object requests, try to create them and equip them + -- Iterate over object requests, try to create them and add them for i, filter in ipairs(t[1]) do - print("Inventory resolver", e.name, e.filter, filter.type, filter.subtype) - local o - if not filter.defined then - o = game.zone:makeEntity(game.level, "object", filter, nil, true) - else - if filter.base_list then - local _, _, class, file = filter.base_list:find("(.*):(.*)") - if class and file then - local base_list = require(class):loadList(file) - base_list.__real_type = "object" - o = game.zone:makeEntityByName(game.level, base_list, filter.defined) + --print("[resolvers.inventory]", e.uid, e.name, (string.fromTable(filter, 1))) + local o = resolvers.resolveObject(e, filter, false) + + if o then + o._resolver_type = t.__resolver + local inven = filter.inven or t[1].inven + print("[resolvers.inventory] created object:", o.uid, o:getName(), "inventory:", inven, "keep:", filter.keep_object) + if inven then inven = e:getInven(inven) or filter.keep_object and e.INVEN_INVEN + else inven = e.INVEN_INVEN + end + if inven then + local id = t[1].id + if filter.id ~= nil then id = filter.id end + if e:addObject(inven, o) or filter.keep_object and inven.id ~= e.INVEN_INVEN and e:addObject(e.INVEN_INVEN, o) then + game.zone:addEntity(game.level, o, "object") + if id ~= nil then o:identify(id) end + if filter.transmo or t[1].transmo then o.__transmo = true end + else + print("[resolvers.inventory] created object:", o.uid, o:getName(), "NOT ADDED") end - else - o = game.zone:makeEntityByName(game.level, "object", filter.defined) end end - if o then --- print("Zone made us an inventory according to filter!", o:getName()) - e:addObject(t[1].inven and e:getInven(t[1].inven) or e.INVEN_INVEN, o) - game.zone:addEntity(game.level, o, "object") - - if t[1].id then o:identify(t[1].id) end - if t[1].transmo then o.__transmo = true end - if t[1].alter then t[1].alter(o) end - end end - e:sortInven() - -- Delete the origin field - return nil + return nil -- Delete the origin field end --- Resolves drops creation for an actor +-- Places objects in main inventory and marks them to be dropped on death +-- @param t a table of object filters to be randomly selected from (resolvers.resolveObject is called for each) +-- additional fields for t: +-- chance = percent chance for drops (all or none, default 100) +-- nb = number of drops (default 1) +-- id: set the identify status of each object function resolvers.drops(t) - return {__resolver="drops", __resolve_last=true, t} + return {__resolver="drops", __resolve_last=true, t, _allow_random_boss=true} end ---- Actually resolve the drops creation + +--- Actually resolve the drops creation adding each object to main inventory +-- @param t the resolver table created by resolvers.drops +-- @param e the entity (Actor) to add the objects to function resolvers.calc.drops(t, e) t = t[1] if not rng.percent(t.chance or 100) then return nil end if t.check and not t.check(e) then return nil end - -- Iterate of object requests, try to create them and drops them + -- Iterate over object requests, adding each object to main inventory and marking it to be dropped for i = 1, (t.nb or 1) do - local filter = t[rng.range(1, #t)] - filter = table.clone(filter) - + local filter = table.clone(t[rng.range(1, #t)]) -- Make sure if we request uniques we do not get lore, it would be kinda deceptive if filter.unique then filter.not_properties = filter.not_properties or {} filter.not_properties[#filter.not_properties+1] = "lore" end - --- print("Drops resolver", e.name, filter.type, filter.subtype, filter.defined) - local o - if not filter.defined then - o = game.zone:makeEntity(game.level, "object", filter, nil, true) - else - local forced - o, forced = game.zone:makeEntityByName(game.level, "object", filter.defined, filter.random_art_replace and true or false) - -- If we forced the generation this means it was already found - if forced then --- print("Serving unique "..o.name.." but forcing replacement drop") - filter.random_art_replace.chance = 100 - end - end + --print("[resolvers.drops]", e.uid, e.name, (string.fromTable(filter, 1))) + local o = resolvers.resolveObject(e, filter, nil) + if o then --- print("Zone made us a drop according to filter!", o:getName()) - e:addObject(e.INVEN_INVEN, o) - game.zone:addEntity(game.level, o, "object") - - if t.id then o:identify(t.id) end - - if filter.random_art_replace then - o.__special_boss_drop = filter.random_art_replace - end + o._resolver_type = "drops" + o.no_drop = false -- make sure it is dropped + if t.id ~= nil then o:identify(t.id) end end end - -- Delete the origin field - return nil + return nil -- Deletes the origin field end -- Resolves material level based on actor level @@ -255,7 +451,7 @@ function resolvers.matlevel(base, base_level, spread, mn, mx) return {__resolver="matlevel", base, base_level, spread, mn, mx} end function resolvers.calc.matlevel(t, e) - local mean = math.min(e.level/10+1,t[1] * (e.level/t[2])^.5) -- I5 material level scales up with sqrt of actor level or level/10 + local mean = math.min(e.level/10+1,t[1] * (e.level/t[2])^.5) -- material level scales up with sqrt of actor level or level/10 local spread = math.max(t[3],mean/5) -- spread out probabilities at high level local mn = t[4] or 1 local mx = t[5] or 5 @@ -274,46 +470,53 @@ local function actor_final_level(e) end end ---- Resolves drops creation for an actor; drops a created on the fly randart +--- Creates a randart and adds it to inventory (to be dropped on death) +-- @param t table of data to generate the randart: +-- @field t._use_object: object to use (bypasses random generation) +-- @field t.filter: optional filter for the base object +-- (defaults to a random, plain object with material level 2 or more) +-- @field t.data: data to pass to game.state:generateRandart +-- (defaults to {lev=resolvers.current_level}) +-- @field t.id: set to identify the randart +-- @field t.no_add: set true to not add the randart to inventory (return it instead when resolved) function resolvers.drop_randart(t) - return {__resolver="drop_randart", __resolve_last=true, t} + return {__resolver="drop_randart", __resolve_last=true, t, _allow_random_boss=true} end ---- Actually resolve the drops creation + +--- Actually resolve the randart +-- @param t the resolver table created by resolvers.drop_randart +-- @param e the entity (Actor) to add the randart to function resolvers.calc.drop_randart(t, e) t = t[1] - local filter = t.filter - - local matresolver = resolvers.matlevel(5,50,1,2) -- Min material level 2 - --- game.log("#LIGHT_BLUE#Calculating randart drop for %s (uid %d, est level %d)",e.name,e.uid,actor_final_level(e)) - if not filter then - filter = {ignore_material_restriction=true, no_tome_drops=true, ego_filter={keep_egos=true, ego_chance=-1000}, special=function(eq) - local matlevel = resolvers.calc.matlevel(matresolver,{level = actor_final_level(e)}) --- game.log("Checking equipment %s against material level %d for %s (final level %d)",eq.name,matlevel,e.name, actor_final_level(e)) - return (not eq.unique and eq.randart_able) and eq.material_level == matlevel and true or false - end} - end - --- print("Randart Drops resolver") - local base = nil - if filter then - if not filter.defined then - base = game.zone:makeEntity(game.level, "object", filter, nil, true) - else - base = game.zone:makeEntityByName(game.level, "object", filter.defined) + local o = t._use_object + if not o then + local data = t.data or {lev=resolvers.current_level} + if not data.base then -- generate a base object + local filter = t.filter + if not filter then + local matresolver = resolvers.matlevel(5,50,1,2) -- Min material level 2 + filter = {ignore_material_restriction=true, no_tome_drops=true, ego_filter={keep_egos=true, ego_chance=-1000}, special=function(eq) + local matlevel = resolvers.calc.matlevel(matresolver,{level = actor_final_level(e)}) + return (not eq.unique and eq.randart_able) and eq.material_level == matlevel and true or false + end} + end + print("[resolvers.drop_randart]", e.uid, e.name, "generating base object using filter:", (string.fromTable(filter, 1))) + data.base = resolvers.resolveObject(e, filter, false, 5) end + print("[resolvers.drop_randart]", e.uid, e.name, "using data:", (string.fromTable(data, 2))) + o = game.state:generateRandart(data) end - - local o = game.state:generateRandart{base=base, lev=resolvers.current_level} if o then --- print("Zone made us a randart drop according to filter!", o:getName{force_id=true}) - e:addObject(e.INVEN_INVEN, o) - game.zone:addEntity(game.level, o, "object") - + o._resolver_type = "drop_randart" + o.no_drop = false if t.id then o:identify(t.id) end + if t.no_add then + return o + else + e:addObject(e.INVEN_INVEN, o) + game.zone:addEntity(game.level, o, "object") + end end - -- Delete the origin field - return nil end --- Resolves drops creation for an actor @@ -350,7 +553,7 @@ end function resolvers.chatfeature(def, faction) return {__resolver="chatfeature", def, faction} end ---- Actually resolve the drops creation +--- Actually resolve the chat creation function resolvers.calc.chatfeature(t, e) e.chat_faction = t[2] t = t[1] @@ -617,7 +820,7 @@ end --- Inscription resolver function resolvers.inscription(name, data, force_id) - return {__resolver="inscription", name, data, force_id} + return {__resolver="inscription", name, data, force_id, _allow_random_boss=true} end function resolvers.calc.inscription(t, e) e:setInscription(t[3] or nil, t[1], t[2], false, false, nil, true, true) @@ -635,7 +838,7 @@ local inscriptions_max = { } function resolvers.inscriptions(nb, list, kind, ignore_limits) - return {__resolver="inscriptions", nb, list, kind, ignore_limits} + return {__resolver="inscriptions", nb, list, kind, ignore_limits, _allow_random_boss=true} end function resolvers.calc.inscriptions(t, e) local kind = nil @@ -705,15 +908,17 @@ end function resolvers.talented_ai_tactic(method, tactic_total, weight_power) local method = method or "simple_recursive" return {__resolver="talented_ai_tactic", method, tactic_total, weight_power, __resolve_last=true, - } + _allow_random_boss=true} end -- Extra recursive methods not handled yet function resolvers.calc.talented_ai_tactic(t, e) local old_on_added_to_level = e.on_added_to_level - e.__ai_compute = t + e.__ai_compute = table.clone(t, false, {__resolver=true}) e.on_added_to_level = function(e, level, x, y) local t = e.__ai_compute +--game.log("#ORANGE# %s calling resolvers.calc.talented_ai_tactic on_added_to_level with %s", e.name, t) + if not t or not (e.x and e.y) then return e.ai_tactic end if old_on_added_to_level then old_on_added_to_level(e, level, x, y) end -- print(" # talented_ai_tactic resolver function for", e.name, "level=", e.level, e.uid) local tactic_total = t[2] or t.tactic_total or 10 --want tactic weights to total 10 @@ -829,8 +1034,8 @@ function resolvers.calc.talented_ai_tactic(t, e) tactic.count = count tactic.level = e.level tactic.type = "computed" ---- print("### talented_ai_tactic resolver ai_tactic table:") ---- for tac, wt in pairs(tactic) do print(" ##", tac, wt) end +-- 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_compute = nil return tactic @@ -838,7 +1043,6 @@ function resolvers.calc.talented_ai_tactic(t, e) end --- Racial Talents resolver - local racials = { halfling = { T_HALFLING_LUCK = {last=10, base=0, every=4, max=5}, @@ -992,4 +1196,3 @@ end function resolvers.calc.command_staff(t, e) e:commandStaff() end - diff --git a/game/profile-thread/Client.lua b/game/profile-thread/Client.lua index dea4d4ac1f21f12b9769d3cb4b795675473d3a1e..c3ab20f1b1bd923ee511475e1075850d6d367c92 100644 --- a/game/profile-thread/Client.lua +++ b/game/profile-thread/Client.lua @@ -392,6 +392,12 @@ function _M:orderSetConfigs(o) end end +function _M:orderSendIncrLog(o) + if not self.auth then cprofile.pushEvent("e='IncrLogConsume' ok=false") return end + self:command("CINC", o.data:len()) + if self:read("200") then self.sock:send(o.data) cprofile.pushEvent("e='IncrLogConsume' ok=true") end +end + function _M:orderSendError(o) o = table.serialize(o) self:command("ERR_", o:len()) diff --git a/tiled-maps/eyal2.tmx b/tiled-maps/eyal2.tmx index e6ae63ef5ef2b0f49b29f1865621f87dd231ada4..b6660fbe0f9255a78bd564a2da9e95dad806e266 100644 --- a/tiled-maps/eyal2.tmx +++ b/tiled-maps/eyal2.tmx @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<map version="1.0" orientation="orthogonal" width="170" height="100" tilewidth="32" tileheight="32"> +<map version="1.0" orientation="orthogonal" renderorder="right-down" width="170" height="100" tilewidth="32" tileheight="32" nextobjectid="113"> <tileset firstgid="1" name="dg_grounds32" tilewidth="32" tileheight="32"> <image source="gfx/dg_grounds32.gif" width="288" height="640"/> <tile id="9"> @@ -313,678 +313,684 @@ eJzt3Ulu3EYUBuBat2HAspML+BBSZMBbAdYygDc2ogPoEgZyhhygDxAvM4/IPOdKCYF+0NPfb6oi2dVNvsWPSGwOxaqvH4vstnJdSrnOZFaWWyHWa7XBffU+38zx5XbGvIQMy17tEtm+d9+sOZtdard7zrZt2Z6P/W1jBmuvWbTX0Sctf8XyWlm3tW38/PCcI+tm7vvkeS6Eb4Ov4fbeMfl4RMaYu+HLuTUr0jqSUbSqmbXcT+U63dpOJXuSW81oi1ct3JdlAteJOJV8al4jx5zTam8jc3qLWLGMoUX0eV7uO7X8Yj2Ojo9V07S6ia+1GuXxjqu1c9j2/cYszallzbPa6vS83DnleaGEO9X6ny+36qLnBWudtI5mkPvSrHpOcf+tTpdm1TN6U7ntzS6S13OWmyLbpG028LpmEpdFPESsRox+sAutj79bRmtrseVRW2dNRs/LfXOR/XCrmlNuMDJftYy2Oo3WN8uoFayxY+cMNU75/La3szms3rBo12fPueSUG8Vreq1TtMl/t5zRMahttVZrnVI0g9RPvF2W04hVnt625vJKNiWj5CV6r07740a5/5r7fsnpbYk7leYc3CkPP6ZmtcaoZpX6hvcr9c83RZ/71swBepuaw+gGxpKPJ/e5gWWW0Rthn+j/RojlFHMrBOundV6a0SEXu3CnksFnu3zHfras8hoqOZX6ouX639vVHEYlU9Ic06qBNK7S3MGL5dSyib9je2uOg/m87FvlDv+F/qM8MyLNdaLv2ZesPZpT/ntvW1Mb5f2szUmlWsqXXZT7TrmNH8o4q1LbuVOKNO41Tr8Vll1AuDla55mw31qnklFsI+/fyL1Ub19T+uROI0a1fqQ+9CxeGOtGaqmWh7vguGtGh2x3kfrjQjg3dIrhtXTY7p+yb1Ry6hnFGqA5XYNRPqbaNdEae88orYP9jftpcfqQRXrfSU63EDRKkfphiGZ1U/ZrrVVL+TF5+7COR5wuaW4q2YvUtdr5JuZHpb/5M4YWp5JRae4RdfpJiTmVrFp+ySk+iyKr6NFatnSnaM4ay4jnSNCl1IaaealmlJxa82TNKT+mVse42Zp7MQpd81udSuc27E+6n+rtbEqn1n2MZFTKOet/2sem7Bs9FaeWUbKi7VM7L+6UL7fuQzW3uHxtTrkzq05o3xNB78O6Z0ZfU97C2L1ly8Y6rXkvej4vi+4U50raPP+FcuyoUylrcMqD73XL5wb2o13v0ak0NtzlW0jEKjrV7tG896B2zb2ESP3H960dC6/53nsk4pT2uWSn2NfoTzKKyy0H5JRbtZxqqXHKxxYNfVFitUx6DZ3y3633abSWthglp8O6S3SqWUWLeF2XxhFrCY7DWfGtSnU0atRyGq2jWr4s+7WUXErLrOO9A/HeI96clJbTeA3H+bsszyla5Ta5Uet+lzvV5gtnZZzViNOviu30s91/fxbaN+QRC75GDt/dhfoFjVrXezQqeY04ldbjToesySnOo6z5pTWfRaM1TnGf1nloz04j9fSREDTKnV6WfauaU9qP5VSyatUFzSn14dKMela5P+8+yLqWSk61fWj3U61O0WqtU26VO5WsXgrHmsIp9hO+9mQX6kNKb1eHsKrVUs2YFdqPdy+FVrnRGqe8rfjMDZ2SS1pfcsqvLzgn1Z7J4XHQ5UXR56o1Tp9AcN3erqaO9jyUO90Uve8k69wof5bgzSGkOarXfs+pdI+ITvkyNCrdS/FrvuaV7+encmeU0uKU1kGjktXerqaM1Df0vMO718T+8JxKz1siVmuc0v6l+oZWpev9o2LXUJyTWs+k8Hmz9VxJsioZpWhO+fF62zqkUyvoFK2i0Uht1q79UafW8yE6L2wPt4qWPaeeUckf9rP13JqyLfvfQdDqKH9/9PY1pVPqQ/6z9VwKE3GKNr37spY5qvTZkDTPls4rcs8fqaU4B5DOD8/bsrpVgttrlnv7mttptKZaTsmcdd3TnH5c6pziewPvZTynw7Jap9znL+xctft+K5LTrRM0zpcv2SnWOukzqFqnWCs8o9xajVPp2BvhuNLna961t3ZeKtVzz2yLU2w3LV+aUcsq9Z1kKuo0OvdCo62fS/HjY1vwvYfPNqJt83xiPbc+7x/jVGo3vbZUpzimdI7W56aSUdqmdvw9p639LlnAOuqZ5D9b92hW6HiWVa+vtmXfqbbOUo3iuOLvNU5rXXpGa2qpdU7SfaHnVPqenueU70OaA2ufi0Xe11tnvW1Zh1NtjKNOcYyxfljzM80obTPmPLSxxWV4Ht780/oOlvb8ydrOc2oZXfpnprVOpXGVnNbMzbhT7do5xqr0/JaPu3ceWjRv2nPSTbnfL7jdddH7RzOKbeptprdT7B9tXezD2rncHE6952GR85DeY5rRjyDDsscs0nZWGz4taTTi1DOK31WiRK6bnl+sO63nUDt3qc012+dg8+kuZPUxxOpTWubdk6ZRfdyiRq3nM9a9ibZdi1Penu0uklXvnDVH0jHRKVmtcYrHWrPRixKvU55RySn1JbeHP0efj5PT88pzHLb5q9x3SmP+Z9GfWYwN1VBuVfMZ6XvLaW9Hc2ZT7pxGrEqvS/8WGePZO4d9SOujYe/cpL8fvC33n+lYc8OprXKvtUavi3zdX4PRIVRHIvcp0nJu66qMm5/iPrz1I07xb4xsy53T70v8/TlFP2Ndfcpe/zqwD/K5Nqf072doftZyP4219EpYZrn0MqdT717bynu71PY5HQed1myvPetdcsgptzqFU/pv1OR58W3ya3+kbdrfabXunWuM8hx63NZklJxSxjqVaunYmolOa89Pc8p/rvkuMbf52/95wELn03tMl5wx/4Y24rTGMLk8K/uf87Sen+Z0U+JzPKyh1E50OuSPEW2V5q+ZceEOr4rvEtfTPGtOKa3t5df62s9wfi13Jnmdl5y21tU0Op/ROcKdap9/Hzr8Gi85neraz61+qLzWe+yPOa3m3uxSMxegv0WB89ep5oCbUu8enT5QMrZt+OxKSm8Lx5qxRnki20lGxzpFm1K8ffxedJtTOfWs9rZwzGm9P6rxOeyL/10fvK8aW0sjTiNWh3uoKU1GzXrj09vIsWSq671XRyWrc13vpfTu55o8nbBvlpSx13zPLa+pvFZPfR6bXWj/m3J6TnM+YCd63W9xOnX9jJwDz6bYVse2d8pzHbZJp+1jNaXVnudwqG3GOD1EX51qvD62zNaM25xjUHvM1nbPdY6HfE8vObVjFPksq2f70umy49mLfO5P68zZrtrv0IyZc4+1OpX1TLxf56w5Y9sj2dXm21N5Taen5VTb7tBOuVXPaPS5W43VXu/lNSZqUOrrKcZgTP1scfqGbYv7bP2Objo9rNOIoTnfIzVmuLMao2i1pXan1fWkpX5qkRzi31KZY76aXpefKcfbM6pZ7e00rR5/Wmqo9v0XzSmtT04vhHV7O02rx53oGF6xaN/Vspxyo5LTqZ8HpNflpcZorVPt/1cw9tlVOl1nPKdnxTYqOZXMtj4TOITNdHoa8Zx638/2rI5N1tCMZdW73tM6c3tNpxnLaU3IdKu/XlZ793tmvFHts1LvM1Ryd+WsX/NdljS67sx5TW1N1tLMKTg9RHr3eyadptPlpbeXdJo5VqvH8D7p3eeZ43U6dztq9tm7zzPHZ/SQ7ezdn5l5MpXBQ9lJp+vMlHXykHbS6boyt9NDtrt3X2bmzZRzv15Oe/dh5jA5xevoKbQxM8+Yn5LTzPqSRjOnkHSaOYWk0cwpJJ1mTiHpNHMKSaeZU0k6zWQymUwmk8lkMplM5vjzH1GIggI= </data> </layer> - <objectgroup name="addSpot#pops" width="170" height="100" visible="0"> - <object name="Allied kingdoms patrol" x="1059" y="1284" width="26" height="24"> + <objectgroup name="addSpot#pops"> + <object id="1" name="Allied kingdoms patrol" x="1059" y="1284" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Allied kingdoms patrol" x="770" y="546" width="26" height="24"> + <object id="2" name="Allied kingdoms patrol" x="770" y="546" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Allied kingdoms patrol" x="546" y="1253" width="26" height="24"> + <object id="3" name="Allied kingdoms patrol" x="546" y="1253" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Allied kingdoms patrol" x="1314" y="577" width="26" height="24"> + <object id="4" name="Allied kingdoms patrol" x="1314" y="577" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Allied kingdoms patrol" x="2081" y="354" width="26" height="24"> + <object id="5" name="Allied kingdoms patrol" x="2081" y="354" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Allied kingdoms patrol" x="1921" y="1219" width="26" height="24"> + <object id="6" name="Allied kingdoms patrol" x="1921" y="1219" width="26" height="24"> <properties> <property name="subtype" value=""allied-kingdoms""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="small hostiles" x="806" y="846" width="48" height="40"> + <object id="7" name="small hostiles" x="806" y="846" width="48" height="40"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""hostile""/> </properties> </object> - <object name="small hostiles" x="1799" y="425" width="46" height="42"> + <object id="8" name="small hostiles" x="1799" y="425" width="46" height="42"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""hostile""/> </properties> </object> - <object name="small hostiles" x="1444" y="1385" width="50" height="48"> + <object id="9" name="small hostiles" x="1444" y="1385" width="50" height="48"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""hostile""/> </properties> </object> - <object name="small hostiles" x="226" y="813" width="52" height="44"> + <object id="10" name="small hostiles" x="226" y="813" width="52" height="44"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""hostile""/> </properties> </object> - <object name="Reknor" x="2212" y="710" width="24" height="20"> + <object id="11" name="Reknor" x="2212" y="710" width="24" height="20"> <properties> <property name="subtype" value=""reknor""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Eruan" x="4962" y="1510" width="24" height="20"> + <object id="12" name="Eruan" x="4962" y="1510" width="24" height="20"> <properties> <property name="subtype" value=""eruan""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Valley of Moon" x="4804" y="1766" width="24" height="20"> + <object id="13" name="Valley of Moon" x="4804" y="1766" width="24" height="20"> <properties> <property name="subtype" value=""valley-moon-caverns""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Gorbat" x="4706" y="1734" width="24" height="20"> + <object id="14" name="Gorbat" x="4706" y="1734" width="24" height="20"> <properties> <property name="subtype" value=""gorbat-pride""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Rak'Shor" x="4486" y="1637" width="24" height="20"> + <object id="15" name="Rak'Shor" x="4486" y="1637" width="24" height="20"> <properties> <property name="subtype" value=""rak-shor-pride""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Ardhungol" x="5184" y="901" width="24" height="20"> + <object id="16" name="Ardhungol" x="5184" y="901" width="24" height="20"> <properties> <property name="subtype" value=""ardhungol""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Lumberjack Town" x="1953" y="708" width="24" height="20"> + <object id="17" name="Lumberjack Town" x="1953" y="708" width="24" height="20"> <properties> <property name="subtype" value=""lumberjack-town""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Telmur" x="1667" y="935" width="24" height="20"> + <object id="18" name="Telmur" x="1667" y="935" width="24" height="20"> <properties> <property name="subtype" value=""telmur""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Vor Pride" x="5378" y="263" width="24" height="20"> + <object id="19" name="Vor Pride" x="5378" y="263" width="24" height="20"> <properties> <property name="subtype" value=""vor-pride""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Vor Armoury" x="5348" y="262" width="24" height="20"> + <object id="20" name="Vor Armoury" x="5348" y="262" width="24" height="20"> <properties> <property name="subtype" value=""vor-armoury""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Grushnak" x="4230" y="326" width="24" height="20"> + <object id="21" name="Grushnak" x="4230" y="326" width="24" height="20"> <properties> <property name="subtype" value=""grushnak-pride""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Briagh Lair" x="5284" y="1670" width="24" height="20"> + <object id="22" name="Briagh Lair" x="5284" y="1670" width="24" height="20"> <properties> <property name="subtype" value=""briagh""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Ruined gates of mroning" x="5218" y="997" width="24" height="20"> + <object id="23" name="Ruined gates of mroning" x="5218" y="997" width="24" height="20"> <properties> <property name="subtype" value=""ruined-gates-of-morning""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Antimagic Town" x="1637" y="520" width="24" height="20"> + <object id="24" name="Antimagic Town" x="1637" y="520" width="24" height="20"> <properties> <property name="subtype" value=""antimagic""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Angolwen" x="386" y="774" width="24" height="20"> + <object id="25" name="Angolwen" x="386" y="774" width="24" height="20"> <properties> <property name="subtype" value=""angolwen""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Angolwen Portal" x="451" y="774" width="24" height="20"> + <object id="26" name="Angolwen Portal" x="451" y="774" width="24" height="20"> <properties> <property name="subtype" value=""angolwen-portal""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="High Peak" x="4869" y="422" width="21" height="18"> + <object id="27" name="High Peak" x="4869" y="422" width="21" height="18"> <properties> <property name="subtype" value=""high-peak""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Far East portal arrival" x="4899" y="517" width="24" height="20"> + <object id="28" name="Far East portal arrival" x="4899" y="517" width="24" height="20"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""farportal-end""/> </properties> </object> - <object name="Gates of morning portal arrival" x="5187" y="999" width="24" height="20"> + <object id="29" name="Gates of morning portal arrival" x="5187" y="999" width="24" height="20"> <properties> <property name="subtype" value=""gates-of-morning""/> <property name="type" value=""farportal-end""/> </properties> </object> - <object name="Last Hope portal arrival" x="1891" y="1254" width="24" height="20"> + <object id="30" name="Last Hope portal arrival" x="1891" y="1254" width="24" height="20"> <properties> <property name="subtype" value=""last-hope""/> <property name="type" value=""farportal-end""/> </properties> </object> - <object name="Iron Throne portal arrival" x="2179" y="712" width="24" height="20"> + <object id="31" name="Iron Throne portal arrival" x="2179" y="712" width="24" height="20"> <properties> <property name="subtype" value=""iron-throne""/> <property name="type" value=""farportal-end""/> </properties> </object> - <object name="Demon Plane portal arrival" x="1891" y="424" width="24" height="20"> + <object id="32" name="Demon Plane portal arrival" x="1891" y="424" width="24" height="20"> <properties> <property name="subtype" value=""demon-plane-arrival""/> <property name="type" value=""farportal-end""/> </properties> </object> - <object name="Sunwall patrol" x="5188" y="1028" width="26" height="24"> + <object id="33" name="Sunwall patrol" x="5188" y="1028" width="26" height="24"> <properties> <property name="subtype" value=""sunwall""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Sunwall patrol" x="5250" y="1348" width="26" height="24"> + <object id="34" name="Sunwall patrol" x="5250" y="1348" width="26" height="24"> <properties> <property name="subtype" value=""sunwall""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Sunwall patrol" x="5218" y="582" width="26" height="24"> + <object id="35" name="Sunwall patrol" x="5218" y="582" width="26" height="24"> <properties> <property name="subtype" value=""sunwall""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Sunwall patrol" x="4836" y="964" width="26" height="24"> + <object id="36" name="Sunwall patrol" x="4836" y="964" width="26" height="24"> <properties> <property name="subtype" value=""sunwall""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="4518" y="1636" width="26" height="24"> + <object id="37" name="Orc pride patrol" x="4518" y="1636" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="4708" y="1699" width="26" height="24"> + <object id="38" name="Orc pride patrol" x="4708" y="1699" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="5218" y="1538" width="26" height="24"> + <object id="39" name="Orc pride patrol" x="5218" y="1538" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="5347" y="293" width="26" height="24"> + <object id="40" name="Orc pride patrol" x="5347" y="293" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="4963" y="548" width="26" height="24"> + <object id="41" name="Orc pride patrol" x="4963" y="548" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Orc pride patrol" x="4227" y="292" width="26" height="24"> + <object id="42" name="Orc pride patrol" x="4227" y="292" width="26" height="24"> <properties> <property name="subtype" value=""orc-pride""/> <property name="type" value=""patrol""/> </properties> </object> - <object name="Zigur" x="1443" y="901" width="24" height="20"> + <object id="43" name="Zigur" x="1443" y="901" width="24" height="20"> <properties> <property name="subtype" value=""zigur""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Sher'Tul Fortress portal" x="1027" y="741" width="24" height="20"> + <object id="44" name="Sher'Tul Fortress portal" x="1027" y="741" width="24" height="20"> <properties> <property name="subtype" value=""shertul-fortress""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Halfling ruins" x="1186" y="1027" width="24" height="20"> + <object id="45" name="Halfling ruins" x="1186" y="1027" width="24" height="20"> <properties> <property name="subtype" value=""halfling-ruins""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Rel tunnel" x="996" y="1510" width="24" height="20"> + <object id="46" name="Rel tunnel" x="996" y="1510" width="24" height="20"> <properties> <property name="subtype" value=""rel-tunnel""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Tempest Peak" x="1923" y="291" width="24" height="20"> + <object id="47" name="Tempest Peak" x="1923" y="291" width="24" height="20"> <properties> <property name="subtype" value=""tempest-peak""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Start: Allied" x="899" y="419" width="26" height="24"> + <object id="48" name="Start: Allied" x="899" y="419" width="26" height="24"> <properties> <property name="subtype" value=""allied""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Start: Dwarves" x="2243" y="740" width="26" height="24"> + <object id="49" name="Start: Dwarves" x="2243" y="740" width="26" height="24"> <properties> <property name="subtype" value=""dwarf""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Start: Shaloren" x="547" y="1347" width="26" height="24"> + <object id="50" name="Start: Shaloren" x="547" y="1347" width="26" height="24"> <properties> <property name="subtype" value=""shaloren""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Start: Yeek" x="866" y="1571" width="26" height="24"> + <object id="51" name="Start: Yeek" x="866" y="1571" width="26" height="24"> <properties> <property name="subtype" value=""yeek""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Start: Low Undeads" x="1250" y="1219" width="26" height="24"> + <object id="52" name="Start: Low Undeads" x="1250" y="1219" width="26" height="24"> <properties> <property name="subtype" value=""low-undead""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Derth" x="805" y="550" width="21" height="18"> + <object id="53" name="Derth" x="805" y="550" width="21" height="18"> <properties> <property name="subtype" value=""derth""/> <property name="type" value=""zone-pop""/> </properties> </object> - <object name="Start: Thaloren" x="1155" y="419" width="26" height="24"> + <object id="54" name="Start: Thaloren" x="1155" y="419" width="26" height="24"> <properties> <property name="subtype" value=""thaloren""/> <property name="type" value=""playerpop""/> </properties> </object> - <object name="Last hope graveyard" x="1989" y="1254" width="24" height="20"> + <object id="55" name="Last hope graveyard" x="1989" y="1254" width="24" height="20"> <properties> <property name="subtype" value=""last-hope-graveyard""/> <property name="type" value=""zone-pop""/> </properties> </object> </objectgroup> - <objectgroup name="addZone#zonenames" width="170" height="100" visible="0"> - <object name="Maj'Eyal" x="39" y="44" width="2478" height="1344"> + <objectgroup name="addZone#zonenames" visible="0"> + <object id="56" name="Maj'Eyal" x="39" y="44" width="2478" height="1344"> <properties> <property name="subtype" value=""Maj'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Maj'Eyal" x="204" y="1406" width="434" height="196"> + <object id="57" name="Maj'Eyal" x="204" y="1406" width="434" height="196"> <properties> <property name="subtype" value=""Maj'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Island of Rel" x="856" y="1458" width="386" height="266"> + <object id="58" name="Island of Rel" x="856" y="1458" width="386" height="266"> <properties> <property name="subtype" value=""Island of Rel""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Charred Scar" x="1668" y="1544" width="306" height="174"> + <object id="59" name="Charred Scar" x="1668" y="1544" width="306" height="174"> <properties> <property name="subtype" value=""Charred Scar""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Far East" x="3870" y="72" width="1542" height="1990"> + <object id="60" name="Far East" x="3870" y="72" width="1542" height="1990"> <properties> <property name="subtype" value=""Far East""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Far East" x="230" y="1736" width="2192" height="1044"> + <object id="61" name="Far East" x="230" y="1736" width="2192" height="1044"> <properties> <property name="subtype" value=""Tar'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Far East" x="370" y="1634" width="332" height="94"> + <object id="62" name="Far East" x="370" y="1634" width="332" height="94"> <properties> <property name="subtype" value=""Tar'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Far East" x="2064" y="1670" width="78" height="56"> + <object id="63" name="Far East" x="2064" y="1670" width="78" height="56"> <properties> <property name="subtype" value=""Tar'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Far East" x="354" y="2786" width="184" height="252"> + <object id="64" name="Far East" x="354" y="2786" width="184" height="252"> <properties> <property name="subtype" value=""Tar'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> - <object name="Maj'Eyal" x="1310" y="1402" width="802" height="120"> + <object id="65" name="Maj'Eyal" x="1310" y="1402" width="802" height="120"> <properties> <property name="subtype" value=""Maj'Eyal""/> <property name="type" value=""zonename""/> </properties> </object> </objectgroup> - <objectgroup name="addZone#pops" width="170" height="100" visible="0"> - <object name="Lumjberjack quest" x="1837" y="747" width="174" height="551"> + <objectgroup name="addZone#pops"> + <object id="66" name="Lumjberjack quest" x="1837" y="747" width="174" height="551"> <properties> <property name="subtype" value=""lumberjack-cursed""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="267" y="459" width="138" height="271"> + <object id="67" name="Trapped merchant quest" x="267" y="459" width="138" height="271"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="77" y="101" width="382" height="305"> + <object id="68" name="Trapped merchant quest" x="77" y="101" width="382" height="305"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="747" y="41" width="240" height="263"> + <object id="69" name="Trapped merchant quest" x="747" y="41" width="240" height="263"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="1197" y="163" width="428" height="145"> + <object id="70" name="Trapped merchant quest" x="1197" y="163" width="428" height="145"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="1803" y="259" width="298" height="185"> + <object id="71" name="Trapped merchant quest" x="1803" y="259" width="298" height="185"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="2221" y="103" width="266" height="465"> + <object id="72" name="Trapped merchant quest" x="2221" y="103" width="266" height="465"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Trapped merchant quest" x="973" y="909" width="172" height="167"> + <object id="73" name="Trapped merchant quest" x="973" y="909" width="172" height="167"> <properties> <property name="subtype" value=""merchant-quest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Orc Breeding Pit quest" x="4645" y="1637" width="728" height="406"> + <object id="74" name="Orc Breeding Pit quest" x="4645" y="1637" width="728" height="406"> <properties> <property name="subtype" value=""orc-breeding-pits""/> <property name="type" value=""world-encounter""/> </properties> </object> </objectgroup> - <objectgroup name="addSpot#world-encounters" width="170" height="100"> - <object name="Random Zone" x="296" y="256" width="50" height="55"> + <objectgroup name="addSpot#world-encounters"> + <object id="75" name="Random Zone" x="296" y="256" width="50" height="55"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="741" y="1253" width="50" height="55"> + <object id="76" name="Random Zone" x="741" y="1253" width="50" height="55"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1958" y="258" width="55" height="25"> + <object id="77" name="Random Zone" x="1958" y="258" width="55" height="25"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="2021" y="583" width="50" height="55"> + <object id="78" name="Random Zone" x="2021" y="583" width="50" height="55"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1701" y="363" width="52" height="45"> + <object id="79" name="Random Zone" x="1701" y="363" width="52" height="45"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Angolwen quest" x="483" y="711" width="120" height="148"> + <object id="80" name="Near Angolwen quest" x="483" y="711" width="120" height="148"> <properties> <property name="subtype" value=""angolwen""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="767" y="169" width="60" height="75"> + <object id="81" name="Random Zone" x="767" y="169" width="60" height="75"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1159" y="777" width="52" height="45"> + <object id="82" name="Random Zone" x="1159" y="777" width="52" height="45"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="325" y="685" width="52" height="45"> + <object id="83" name="Random Zone" x="325" y="685" width="52" height="45"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1193" y="229" width="140" height="20"> + <object id="84" name="Random Zone" x="1193" y="229" width="140" height="20"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="2247" y="263" width="52" height="45"> + <object id="85" name="Random Zone" x="2247" y="263" width="52" height="45"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="2278" y="521" width="52" height="45"> + <object id="86" name="Random Zone" x="2278" y="521" width="52" height="45"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1769" y="743" width="50" height="55"> + <object id="87" name="Random Zone" x="1769" y="743" width="50" height="55"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="1989" y="1445" width="50" height="55"> + <object id="88" name="Random Zone" x="1989" y="1445" width="50" height="55"> <properties> <property name="subtype" value=""maj-eyal""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Mark of the Spellblaze" x="1312" y="1280" width="160" height="160"> + <object id="89" name="Mark of the Spellblaze" x="1312" y="1280" width="160" height="160"> <properties> <property name="subtype" value=""mark-spellblaze""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="5088" y="608" width="96" height="64"> + <object id="90" name="Random Zone" x="5088" y="608" width="96" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4928" y="320" width="64" height="32"> + <object id="91" name="Random Zone" x="4928" y="320" width="64" height="32"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4288" y="352" width="96" height="64"> + <object id="92" name="Random Zone" x="4288" y="352" width="96" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4096" y="960" width="64" height="64"> + <object id="93" name="Random Zone" x="4096" y="960" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4320" y="1024" width="64" height="64"> + <object id="94" name="Random Zone" x="4320" y="1024" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4896" y="1280" width="64" height="64"> + <object id="95" name="Random Zone" x="4896" y="1280" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="5344" y="1248" width="64" height="64"> + <object id="96" name="Random Zone" x="5344" y="1248" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="5216" y="1504" width="64" height="64"> + <object id="97" name="Random Zone" x="5216" y="1504" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4416" y="1408" width="64" height="64"> + <object id="98" name="Random Zone" x="4416" y="1408" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4896" y="896" width="64" height="96"> + <object id="99" name="Random Zone" x="4896" y="896" width="64" height="96"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="4864" y="160" width="32" height="64"> + <object id="100" name="Random Zone" x="4864" y="160" width="32" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Random Zone" x="5088" y="1376" width="64" height="64"> + <object id="101" name="Random Zone" x="5088" y="1376" width="64" height="64"> <properties> <property name="subtype" value=""fareast""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Brotherhood of alchemists" x="1220" y="265" width="19" height="19"> + <object id="102" name="Brotherhood of alchemists" x="1220" y="265" width="19" height="19"> <properties> <property name="subtype" value=""brotherhood-alchemist""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Brotherhood of alchemists" x="391" y="455" width="19" height="19"> + <object id="103" name="Brotherhood of alchemists" x="391" y="455" width="19" height="19"> <properties> <property name="subtype" value=""brotherhood-alchemist""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Brotherhood of alchemists" x="1125" y="903" width="19" height="19"> + <object id="104" name="Brotherhood of alchemists" x="1125" y="903" width="19" height="19"> <properties> <property name="subtype" value=""brotherhood-alchemist""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Brotherhood of alchemists" x="2055" y="323" width="19" height="19"> + <object id="105" name="Brotherhood of alchemists" x="2055" y="323" width="19" height="19"> <properties> <property name="subtype" value=""brotherhood-alchemist""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Brotherhood of alchemists" x="2375" y="1031" width="19" height="19"> + <object id="106" name="Brotherhood of alchemists" x="2375" y="1031" width="19" height="19"> <properties> <property name="subtype" value=""brotherhood-alchemist""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Lost merchant quest" x="1034" y="683" width="171" height="135"> + <object id="107" name="Lost merchant quest" x="1034" y="683" width="171" height="135"> <properties> <property name="subtype" value=""lost-merchant""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Noxious Caldera" x="1125" y="1125" width="21" height="20"> + <object id="108" name="Noxious Caldera" x="1125" y="1125" width="21" height="20"> <properties> <property name="subtype" value=""noxious-caldera""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Sludgenest" x="1703" y="166" width="21" height="20"> + <object id="109" name="Sludgenest" x="1703" y="166" width="21" height="20"> <properties> <property name="subtype" value=""sludgenest""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Breeding pits Spawn" x="5190" y="1894" width="17" height="19"> + <object id="110" name="Breeding pits Spawn" x="5190" y="1894" width="17" height="19"> <properties> <property name="subtype" value=""orc-breeding-pits-spawn""/> <property name="type" value=""world-encounter""/> </properties> </object> - <object name="Conclave vault" x="1508" y="1093" width="21" height="20"> + <object id="111" name="Conclave vault" x="1508" y="1093" width="21" height="20"> <properties> <property name="subtype" value=""conclave-vault""/> <property name="type" value=""world-encounter""/> </properties> </object> + <object id="112" name="Angolwen quest" x="452" y="802" width="23" height="23"> + <properties> + <property name="subtype" value=""angolwen-quest""/> + <property name="type" value=""world-encounter""/> + </properties> + </object> </objectgroup> </map>