diff --git a/game/engines/default/engine/Zone.lua b/game/engines/default/engine/Zone.lua index 8f612c0f2b67a8c0917bb470c098b1bdee2d6166..6335ed3816cad996544e85049dee0c8d6d14748f 100644 --- a/game/engines/default/engine/Zone.lua +++ b/game/engines/default/engine/Zone.lua @@ -30,6 +30,18 @@ module(..., package.seeall, class.make) _no_save_fields = {temp_memory_levels=true, _tmp_data=true} + +--- List of rules to run through when applying an ego to an entity. +_M.ego_rules = {} + +--- Adds an ego rule. +-- Static method +function _M:addEgoRule(kind, rule) + self.ego_rules[kind] = self.ego_rules[kind] or {} + table.insert(self.ego_rules[kind], rule) +end + + --- Setup classes to use for level generation -- Static method -- @param t table that contains the name of the classes to use @@ -361,7 +373,7 @@ function _M:makeEntity(level, type, filter, force_level, prob_filter) -- Generate a specific probability list, slower to generate but no need to "try and be lucky" elseif filter then local base_list = nil - if filter.base_list then + if filter.base_list then if _G.type(filter.base_list) == "table" then base_list = filter.base_list else local _, _, class, file = filter.base_list:find("(.*):(.*)") @@ -433,7 +445,7 @@ local pick_ego = function(self, level, e, eegos, egos_list, type, picked_etype, if _G.type(etype) == "number" then etype = "" end local egos = e.egos and level:getEntitiesList(type.."/"..e.egos..":"..etype) - + if not egos then egos = self:generateEgoEntities(level, type, etype, eegos, e.__CLASSNAME) end if self.ego_filter then ego_filter = self.ego_filter(self, level, type, etype, e, ego_filter, egos_list, picked_etype) end @@ -475,8 +487,8 @@ function _M:finishEntity(level, type, e, ego_filter) ego.instant_resolve = nil -- Void the uid, we dont want to erase the base entity's one ego.uid = nil - -- Merge additively but with array appending, so that nameless resolvers are not lost - table.mergeAddAppendArray(e, ego, true) + -- Merge according to Object's ego rules. + table.ruleMergeAppendAdd(e, ego, self.ego_rules[type] or {}) e.name = newname e.egoed = true end @@ -563,8 +575,8 @@ function _M:finishEntity(level, type, e, ego_filter) ego.instant_resolve = nil -- Void the uid, we dont want to erase the base entity's one ego.uid = nil - -- Merge additively but with array appending, so that nameless resolvers are not lost - table.mergeAddAppendArray(e, ego, true) + -- Merge according to Object's ego rules. + table.ruleMergeAppendAdd(e, ego, self.ego_rules[type] or {}) e.name = newname e.egoed = true end diff --git a/game/engines/default/engine/utils.lua b/game/engines/default/engine/utils.lua index 97f4a598ea02569646bc734ae3a5fb3254bbc1bc..a2601c2ea6f6a865774a231f0e045bfdc5fd2988 100644 --- a/game/engines/default/engine/utils.lua +++ b/game/engines/default/engine/utils.lua @@ -286,6 +286,83 @@ function table.readonly(src) }); end +-- Make a new table with each k, v = f(k, v) in the original. +function table.map(f, source) + local result = {} + for k, v in pairs(source) do + k2, v2 = f(k, v) + result[k2] = v2 + end + return result +end + +-- Make a new table with each k, v = k, f(v) in the original. +function table.mapv(f, source) + local result = {} + for k, v in pairs(source) do + result[k] = f(v) + end + return result +end + +-- Find the keys that are only in left, only in right, and are common +-- to both. +function table.compareKeys(left, right) + local result = {left = {}, right = {}, both = {}} + for k, _ in pairs(left) do + if right[k] then + result.both[k] = true + else + result.left[k] = true + end + end + for k, _ in pairs(right) do + if not left[k] then + result.right[k] = true + end + end + return result +end + +--[=[ + Decends recursively through a table by the given list of keys. + + 1st return: The first non-table value found, or the final value if + we ran out of keys. + + 2nd return: If the list of keys was exhausted + + Meant to replace multiple ands to get a value: + "a and a.b and a.b.c" turns to "rget(a, 'b', 'c')" +]=] +function table.get(table, ...) + if type(table) ~= 'table' then return table, false end + for _, key in ipairs({...}) do + if type(table) ~= 'table' then return table, false end + table = table[key] + end + return table, true +end + +--[=[ + Set the nested value in a table, creating empty tables as needed. +]=] +function table.set(table, ...) + if type(table) ~= 'table' then return false end + local args = {...} + for i = 1, #args - 2 do + local key = args[i] + local subtable = table[key] + if not subtable then + subtable = {} + table[key] = subtable + end + table = subtable + end + table[args[#args - 1]] = args[#args] +end + + -- Taken from http://lua-users.org/wiki/SortedIteration and modified local function cmp_multitype(op1, op2) local type1, type2 = type(op1), type(op2) @@ -318,6 +395,22 @@ function table.orderedPairs(t) end end +-- ordering is a function({k1, v1}, {k2, v2}), it should return true +-- when left is < right. +function table.orderedPairs(t, ordering) + if not next(t) then return function() end end + t = table.listify(t) + if #t > 1 then table.sort(t, ordering) end + local index = 1 + return function() + if index <= #t then + value = t[index] + index = index + 1 + return value[1], value[2] + end + end +end + --- Shuffles the content of a table (list) function table.shuffle(t) local n = #t @@ -328,6 +421,89 @@ function table.shuffle(t) return t end +-- Common table rules. +table.rules = {} + +--[[ +Applies a series of rules to a pair of tables. rules should be a list of functions +to be applied, in order, until one returns true. + +All keys in the table src are looped through, starting with array +indices, and then the rest of the keys. The rules are given the +following arguments: +The dst table's value for the current key. +The src table's value for the current key. +The current key. +The dst table. +The src table. +The list of rules. +A state table which the rules are free to modify. +--]] +function table.applyRules(dst, src, rules, state) + if not dst or not src then return end + state = state or {} + local used_keys = {} + -- First loop through with ipairs so we get the numbers in order. + for k, v in ipairs(src) do + used_keys[k] = true + for _, rule in ipairs(rules) do + if rule(dst[k], src[k], k, dst, src, rules, state) then + break + end + end + end + -- Then loop through with pairs, skipping the ones we got with ipairs. + for k, v in pairs(src) do + if not used_keys[k] then + for _, rule in ipairs(rules) do + if rule(dst[k], src[k], k, dst, src, rules, state) then + break + end + end + end + end +end + +-- Simply overwrites the value. +table.rules.overwrite = function(dvalue, svalue, key, dst) + dst[key] = svalue + return true +end +-- Does the recursion. +table.rules.recurse = function(dvalue, svalue, key, dst, src, rules, state) + if type(dvalue) ~= 'table' or type(svalue) ~= 'table' then return end + state = table.clone(state) + state.path = table.clone(state.path) or {} + table.insert(state.path, key) + table.applyRules(dvalue, svalue, rules, state) + return true +end +-- Appends indices. +table.rules.append = function(dvalue, svalue, key, dst, src, rules, state) + if type(key) ~= 'number' then return end + table.insert(dst, svalue) + return true +end +-- Adds numbers +table.rules.add = function(dvalue, svalue, key, dst) + if type(dvalue) ~= 'number' or type(svalue) ~= 'number' then return end + dst[key] = dvalue + svalue + return true +end + +--[[ +A convenience method for merging tables, appending numeric indices, +and adding number values in addition to other rules. +--]] +function table.ruleMergeAppendAdd(dst, src, rules) + rules = table.clone(rules) + for _, rule in pairs {'append', 'recurse', 'add', 'overwrite'} do + table.insert(rules, table.rules[rule]) + end + table.applyRules(dst, src, rules) +end + + function string.ordinal(number) local suffix = "th" number = tonumber(number) @@ -1921,7 +2097,7 @@ function util.showMainMenu(no_reboot, reboot_engine, reboot_engine_version, rebo core.steam.cancelGrabSubscribedAddons() core.steam.sessionTicketCancel() end - + -- Tell the C engine to discard the current lua state and make a new one print("[MAIN] rebooting lua state: ", reboot_engine, reboot_engine_version, reboot_module, reboot_name, reboot_new) core.game.reboot("te4core", -1, reboot_engine or "te4", reboot_engine_version or "LATEST", reboot_module or "boot", reboot_name or "player", reboot_new, reboot_einfo or "") @@ -2041,4 +2217,3 @@ function require_first(...) end return nil end - diff --git a/game/modules/tome/class/Object.lua b/game/modules/tome/class/Object.lua index 32c5c9a24dbca88d81a33cc0fed8cefa079236eb..7712b7e69ceef7828c93b56cc18e7ee2cb3e59f3 100644 --- a/game/modules/tome/class/Object.lua +++ b/game/modules/tome/class/Object.lua @@ -443,7 +443,7 @@ function _M:getTextualDesc(compare_with, use_actor) end end - local compare_table_fields = function(item1, items, infield, field, outformat, text, kfunct, mod, isinversed) + 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{} @@ -467,6 +467,7 @@ function _M:getTextualDesc(compare_with, use_actor) end local count1 = 0 for k, v in pairs(tab) do + if filter and not filter(k, v) then goto filtered end 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"}) @@ -501,6 +502,7 @@ function _M:getTextualDesc(compare_with, use_actor) ret:add(")") end ret:add(kfunct(k)) + ::filtered:: end if add then @@ -556,7 +558,7 @@ function _M:getTextualDesc(compare_with, use_actor) if combat.wil_attack then desc:add("Accuracy is based on willpower for this weapon.", true) end - + if combat.is_psionic_focus then desc:add("This weapon will act as a psionic focus.", true) end @@ -592,7 +594,7 @@ function _M:getTextualDesc(compare_with, use_actor) 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 @@ -630,82 +632,125 @@ function _M:getTextualDesc(compare_with, use_actor) end --]] - -- Store the DamageTypes with tdesc defined in dt_string - -- Store the DamageTypes without tdesc in combat2 (to be displayed normally) - local found = false - local dt_string = tstring{} - local combat2 = { melee_project = {} } - for i, v in pairs(combat.melee_project or {}) do - local def = DamageType.dam_def[i] - if def and def.tdesc then - local d = def.tdesc(v) - found = true - dt_string:add(d, {"color","LAST"}, true) - - else - combat2.melee_project[i] = v + -- 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 - 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 - local ranged_string = tstring{} - local ranged_combat = { ranged_project = {} } - for i, v in pairs(combat.ranged_project or {}) do - local def = DamageType.dam_def[i] - if def and def.tdesc then - local d = def.tdesc(v) - found = true - ranged_string:add(d, {"color","LAST"}, true) - + -- 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.orderedPairs(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 + prefix = '* ' + 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 - ranged_combat.ranged_project[i] = v + local items = get_items(combat) + if next(items) then + desc:add(header, true) + for k, v in table.orderedPairs(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 - -- Add the on hit section only if theres a tdesc DT or a special_on_hit - if found or special ~= "" then - desc:add({"color","ORANGE"}, "When this weapon hits: ", {"color","LAST"}, true) + local get_special_list = function(combat, key) + local special = combat[key] - end + -- No special + if not special then return {} end - -- Add the special_on_hit desc first always in green - if special ~= "" then - desc:add({"color","GREEN"}, special:capitalize(), {"color","LAST"}, true) - end - - -- Add the extended melee_project descriptions after special_on_hit, colors defined by tdesc - if found then - desc:merge(dt_string) - desc:merge(ranged_string) - end + -- Single special + if special.desc then + return {[special.desc] = {10, special.desc}} + end - -- only special_on_hit display is modified, not special_on_crit and so on - special = "" - if combat.special_on_crit then - special = combat.special_on_crit.desc - end - found = false - for i, v in ipairs(compare_with or {}) do - if v[field] and v[field].special_on_crit then - if special ~= v[field].special_on_crit.desc then - desc:add({"color","RED"}, "When this weapon crits: "..v[field].special_on_crit.desc, {"color","LAST"}, true) - else - found = true + -- Multiple specials + local list = {} + for _, special in pairs(special) do + list[special.desc] = {10, special.desc} + end + return list + end + + compare_list( + "On weapon hit:", + function(combat) + 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 - end - if special ~= "" then - desc:add(found and {"color","WHITE"} or {"color","ORANGE"}, "When this weapon crits: "..special, {"color","LAST"}, true) - end + ) - local special = "" - if combat.special_on_kill then - special = combat.special_on_kill.desc - end + compare_list( + "On weapon crit:", + function(combat) + return get_special_list(combat, 'special_on_crit') + end + ) - if special ~= "" then - desc:add(found and {"color","WHITE"} or {"color","ORANGE"}, "When this weapon kills: "..special, {"color","LAST"}, true) - end + compare_list( + "On weapon kill:", + function(combat) + return get_special_list(combat, 'special_on_kill') + end + ) found = false for i, v in ipairs(compare_with or {}) do @@ -719,7 +764,7 @@ function _M:getTextualDesc(compare_with, use_actor) 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 @@ -727,23 +772,30 @@ function _M:getTextualDesc(compare_with, use_actor) 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) if combat.tg_type and combat.tg_type == "beam" then desc:add({"color","YELLOW"}, ("Shots beam through all targets."), {"color","LAST"}, true) end - -- Use the second combat table for melee_project so we don't repeat the tdesc entries - compare_table_fields(combat2, compare_with, field, "melee_project", "%+d", "Damage (Melee): ", function(item) + 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) + end, + nil, nil, + function(k, v) return not DamageType.dam_def[k].tdesc end) - compare_table_fields(ranged_combat, compare_with, field, "ranged_project", "%+d", "Damage (Ranged): ", function(item) + 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) + 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() @@ -1181,7 +1233,7 @@ function _M:getTextualDesc(compare_with, use_actor) compare_fields(w, compare_with, field, "infravision", "%+d", "Infravision radius: ") compare_fields(w, compare_with, field, "heightened_senses", "%+d", "Heightened senses radius: ") compare_fields(w, compare_with, field, "sight", "%+d", "Sight radius: ") - + compare_fields(w, compare_with, field, "see_stealth", "%+d", "See stealth: ") compare_fields(w, compare_with, field, "see_invisible", "%+d", "See invisible: ") @@ -1218,10 +1270,10 @@ function _M:getTextualDesc(compare_with, use_actor) compare_fields(w, compare_with, field, "nature_summon_max", "%+d", "Max wilder summons: ") compare_fields(w, compare_with, field, "nature_summon_regen", "%+.2f", "Life regen bonus (wilder-summons): ") - + compare_fields(w, compare_with, field, "shield_dur", "%+d", "Damage Shield Duration: ") compare_fields(w, compare_with, field, "shield_factor", "%+d%%", "Damage Shield Power: ") - + compare_fields(w, compare_with, field, "iceblock_pierce", "%+d%%", "Ice block penetration: ") compare_fields(w, compare_with, field, "slow_projectiles", "%+d%%", "Slows Projectiles: ") @@ -1229,13 +1281,13 @@ function _M:getTextualDesc(compare_with, use_actor) compare_fields(w, compare_with, field, "paradox_reduce_fails", "%+d", "Reduces paradox failures(equivalent to willpower): ") compare_fields(w, compare_with, field, "damage_backfire", "%+d%%", "Damage Backlash: ", nil, true) - + compare_fields(w, compare_with, field, "resist_unseen", "%-d%%", "Reduce all damage from unseen attackers: ") if w.undead then desc:add("The wearer is treated as an undead.", true) end - + if w.demon then desc:add("The wearer is treated as a demon.", true) end @@ -1243,7 +1295,7 @@ function _M:getTextualDesc(compare_with, use_actor) if w.blind then desc:add("The wearer is blinded.", true) end - + if w.sleep then desc:add("The wearer is asleep.", true) end @@ -1251,7 +1303,7 @@ function _M:getTextualDesc(compare_with, use_actor) if w.blind_fight then desc:add({"color", "YELLOW"}, "Blind-Fight: ", {"color", "LAST"}, "This item allows the wearer to attack unseen targets without any penalties.", true) end - + if w.lucid_dreamer then desc:add({"color", "YELLOW"}, "Lucid Dreamer: ", {"color", "LAST"}, "This item allows the wearer to act while sleeping.", true) end @@ -1259,7 +1311,7 @@ function _M:getTextualDesc(compare_with, use_actor) if w.no_breath then desc:add("The wearer no longer has to breathe.", true) end - + if w.quick_weapon_swap then desc:add({"color", "YELLOW"}, "Quick Weapon Swap:", {"color", "LAST"}, "This item allows the wearer to swap to their secondary weapon without spending a turn.", true) end diff --git a/game/modules/tome/class/Zone.lua b/game/modules/tome/class/Zone.lua index 69c025dd50aca570c6325fb383d4c1a321704d88..efa81099d127abc667fea694c26b934c6fffc1b5 100644 --- a/game/modules/tome/class/Zone.lua +++ b/game/modules/tome/class/Zone.lua @@ -25,6 +25,24 @@ module(..., package.seeall, class.inherit(Zone)) _M:enableLastPersistZones(3) +-- Merge special_on_crit values. +_M:addEgoRule("object", function(dvalue, svalue, key, dst, src, rules, state) + -- Only work on the special_on_* keys. + if key ~= 'special_on_hit' and key ~= 'special_on_crit' and key ~= 'special_on_kill' then return end + -- If the special isn't a table, make it an empty one. + if type(dvalue) ~= 'table' then dvalue = {} end + if type(svalue) ~= 'table' then svalue = {} end + -- If the special is a single special, wrap it to allow multiple. + if dvalue.fct then dvalue = {dvalue} end + if svalue.fct then svalue = {svalue} end + -- Save changes to the specials. + dst[key] = dvalue + src[key] = svalue + -- Return false so we can let the normal table merge take care of + -- the rest. + return false +end) + --- Called when the zone file is loaded function _M:onLoadZoneFile(basedir) -- Load events if they exist diff --git a/game/modules/tome/class/interface/Combat.lua b/game/modules/tome/class/interface/Combat.lua index cc75d8f9dc4995e9ec688ccaff7f4e01eb734998..aa9c71f16228575a171ea2fd56050800bd0b7813 100644 --- a/game/modules/tome/class/interface/Combat.lua +++ b/game/modules/tome/class/interface/Combat.lua @@ -342,7 +342,7 @@ end function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) damtype = damtype or (weapon and weapon.damtype) or DamageType.PHYSICAL mult = mult or 1 - + --Life Steal if weapon and weapon.lifesteal then self:attr("lifesteal", weapon.lifesteal) @@ -351,7 +351,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) local mode = "other" if self:hasShield() then mode = "shield" - elseif self:hasTwoHandedWeapon() then mode = "twohanded" + elseif self:hasTwoHandedWeapon() then mode = "twohanded" elseif self:hasDualWeapon() then mode = "dualwield" end self.turn_procs.weapon_type = {kind=weapon and weapon.talented or "unknown", mode=mode} @@ -426,7 +426,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) game:delayedLogDamage(self, target, 0, ("%s(%d parried#LAST#)"):format(DamageType:get(damtype).text_color or "#aaaaaa#", deflect), false) dam = math.max(dam - deflect,0) print("[ATTACK] after DUAL_WEAPON_DEFENSE", dam) - end + end end if target.knowTalent and target:hasEffect(target.EFF_GESTURE_OF_GUARDING) and not target:attr("encased_in_ice") then local deflected = math.min(dam, target:callTalent(target.T_GESTURE_OF_GUARDING, "doGuard")) or 0 @@ -487,10 +487,10 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) if weapon and weapon.phasing then self:attr("damage_shield_penetrate", weapon.phasing) end - + local oldproj = DamageType:getProjectingFor(self) if self.__talent_running then DamageType:projectingFor(self, {project_type={talent=self.__talent_running}}) end - + if weapon and weapon.crushing_blow then self:attr("crushing_blow", 1) end -- Damage conversion? @@ -515,7 +515,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) if dam > 0 then DamageType:get(damtype).projector(self, target.x, target.y, damtype, math.max(0, dam)) end - + if weapon and weapon.crushing_blow then self:attr("crushing_blow", -1) end if self.__talent_running then DamageType:projectingFor(self, oldproj) end @@ -658,7 +658,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) self:incStamina(-8) self.shattering_impact_last_turn = game.turn end - + -- Damage Backlash if dam > 0 and self.attr and self:attr("damage_backfire") then local hurt = math.min(dam, target.life) * self.damage_backfire / 100 @@ -772,13 +772,35 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) end -- Special effect - if hitted and weapon and weapon.special_on_hit and weapon.special_on_hit.fct and (not target.dead or weapon.special_on_hit.on_kill) then - weapon.special_on_hit.fct(weapon, self, target, dam) + if hitted and weapon and weapon.special_on_hit then + local specials = weapon.special_on_hit + if specials.fct then specials = {specials} end + for _, special in ipairs(specials) do + if special.fct and (not target.dead or special.on_kill) then + special.fct(weapon, self, target, dam) + end + end + end + + if hitted and crit and weapon and weapon.special_on_crit then + local specials = weapon.special_on_crit + if specials.fct then specials = {specials} end + for _, special in ipairs(specials) do + if special.fct and (not target.dead or special.on_kill) then + special.fct(weapon, self, target, dam) + end + end end - if hitted and crit and weapon and weapon.special_on_crit and weapon.special_on_crit.fct and (not target.dead or weapon.special_on_crit.on_kill) then - weapon.special_on_crit.fct(weapon, self, target, dam) - end + if hitted and weapon and weapon.special_on_kill and target.dead then + local specials = weapon.special_on_kill + if specials.fct then specials = {specials} end + for _, special in ipairs(specials) do + if special.fct then + special.fct(weapon, self, target, dam) + end + end + end if hitted and weapon and weapon.special_on_kill and weapon.special_on_kill.fct and target.dead then weapon.special_on_kill.fct(weapon, self, target, dam) @@ -836,9 +858,9 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) if cadam then game.logSeen(self, "%s counters the attack!", target.name:capitalize()) target:attackTarget(self, nil, cadam, true) - end - end - + end + end + -- Gesture of Guarding counterattack if hitted and not target.dead and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:hasEffect(target.EFF_GESTURE_OF_GUARDING) then local t = target:getTalentFromId(target.T_GESTURE_OF_GUARDING) @@ -868,8 +890,8 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) -- Roll with it if hitted and target:attr("knockback_on_hit") and not target.turn_procs.roll_with_it and rng.percent(util.bound(dam, 0, 100)) then local ox, oy = self.x, self.y - game:onTickEnd(function() - target:knockback(ox, oy, 1) + game:onTickEnd(function() + target:knockback(ox, oy, 1) if not target:hasEffect(target.EFF_WILD_SPEED) then target:setEffect(target.EFF_WILD_SPEED, 1, {power=200}) end end) target.turn_procs.roll_with_it = true @@ -959,7 +981,7 @@ function _M:attackTargetWith(target, weapon, damtype, mult, force_dam) self.turn_procs.weapon_type = nil self.__global_accuracy_damage_bonus = nil - + --Life Steal if weapon and weapon.lifesteal then self:attr("lifesteal", -weapon.lifesteal) @@ -1005,7 +1027,7 @@ function _M:combatGetTraining(weapon) if type(_M.weapon_talents[weapon.talented]) == "table" then local ktid, max = _M.weapon_talents[weapon.talented][1], self:getTalentLevel(_M.weapon_talents[weapon.talented][1]) for i, tid in ipairs(_M.weapon_talents[weapon.talented]) do - if self:knowTalent(tid) then + if self:knowTalent(tid) then if self:getTalentLevel(tid) > max then ktid = tid max = self:getTalentLevel(tid) @@ -1063,7 +1085,7 @@ function _M:combatDefenseBase(fake) end local d = math.max(0, self.combat_def + (self:getDex() - 10) * 0.35 + (self:getLck() - 50) * 0.4) local mult = 1 - + if self:hasLightArmor() and self:knowTalent(self.T_MOBILE_DEFENCE) then mult = mult + self:callTalent(self.T_MOBILE_DEFENCE,"getDef") end @@ -1310,7 +1332,7 @@ function _M:combatTalentScale(t, low, high, power, add, shift, raw) if power == "log" then -- always >= 0 return math.max(0, m * math.log10(tl + shift) + b + add) -- return math.max(0, m * math.log10(tl + shift) + b + add), m, b - else + else return math.max(0, m * (tl + shift)^power + b + add) -- return math.max(0, m * (tl + shift)^power + b + add), m, b end @@ -1339,7 +1361,7 @@ function _M:combatStatScale(stat, low, high, power, add, shift) if power == "log" then -- always >= 0 return math.max(0, m * math.log10(stat + shift) + b + add) -- return math.max(0, m * math.log10(stat + shift) + b + add), m, b - else + else return math.max(0, m * (stat + shift)^power + b + add) -- return math.max(0, m * (stat + shift)^power + b + add), m, b end @@ -1901,11 +1923,11 @@ function _M:combatMentalResist(fake) local d = self.combat_mentalresist + (self:getCun() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.35 + add if self:attr("dazed") then d = d / 2 end - + local nm = self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES) if nm and rng.percent(20) and not fake then d = d * (1-self.tempeffect_def.EFF_CURSE_OF_NIGHTMARES.getVisionsReduction(nm, nm.level)/100) - end + end return self:rescaleCombatStats(d) end @@ -1923,8 +1945,8 @@ end --- Returns the resistance function _M:combatGetResist(type) local power = 100 - if self.force_use_resist and self.force_use_resist ~= type then - type = self.force_use_resist + if self.force_use_resist and self.force_use_resist ~= type then + type = self.force_use_resist power = self.force_use_resist_percent or 100 end @@ -2321,5 +2343,5 @@ end function _M:logCombat(target, style, ...) if not game.uiset or not game.uiset.logdisplay then return end local visible, srcSeen, tgtSeen = game:logVisible(self, target) -- should a message be displayed? - if visible then game.uiset.logdisplay(game:logMessage(self, srcSeen, target, tgtSeen, style, ...)) end + if visible then game.uiset.logdisplay(game:logMessage(self, srcSeen, target, tgtSeen, style, ...)) end end diff --git a/game/modules/tome/data/damage_types.lua b/game/modules/tome/data/damage_types.lua index a0a9cfff412a79a46be5e1b7427962f15ae48dbb..022d695c2cefea79fb7993ce720d2223ae5d9da6 100644 --- a/game/modules/tome/data/damage_types.lua +++ b/game/modules/tome/data/damage_types.lua @@ -47,7 +47,7 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) src.elemental_mastery = old return dam end - + if src:attr("twilight_mastery") then local ndam = dam * src.twilight_mastery local old = src.twilight_mastery @@ -236,7 +236,7 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) if dam ~= lastdam then game:delayedLogDamage(src, target, 0, ("%s(%d to psi shield)#LAST#"):format(DamageType:get(type).text_color or "#aaaaaa#", lastdam-dam), false) end - + --target.T_STONE_FORTRESS could be checked/applied here (ReduceDamage function in Dwarven Fortress talent) -- Damage Smearing @@ -263,8 +263,8 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) if src:checkClassification(tostring(k)) then res = math.max(res, v) end end - res = math.min(res, target.resists_cap_actor_type or 90) - + res = math.min(res, target.resists_cap_actor_type or 90) + if res ~= 0 then print("[PROJECTOR] before entity", src.type, "resists dam", dam) if res >= 100 then dam = 0 @@ -310,7 +310,7 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) dam= dam - demon_block target:incVim((-demon_block)/20) end - + -- Static reduce damage if dam > 0 and target.isTalentActive and target:isTalentActive(target.T_ANTIMAGIC_SHIELD) then local t = target:getTalentFromId(target.T_ANTIMAGIC_SHIELD) @@ -359,16 +359,16 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) local def = src.tempeffect_def[src.EFF_CURSE_OF_MISFORTUNE] dam = def.doUnfortunateEnd(src, eff, target, dam) end - + if src:attr("crushing_blow") and (dam * (1.25 + (src.combat_critical_power or 0)/200)) > target.life then dam = dam * (1.25 + (src.combat_critical_power or 0)/200) game.logPlayer(src, "You end your target with a crushing blow!") end - + if target:attr("resist_unseen") and not target:canSee(src) then dam = dam * (1 - math.min(target.resist_unseen,100)/100) end - + -- Sanctuary: reduces damage if it comes from outside of Gloom if target.isTalentActive and target:isTalentActive(target.T_GLOOM) and target:knowTalent(target.T_SANCTUARY) then if tmp and tmp.sanctuaryDamageChange then @@ -397,7 +397,7 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) print("[PROJECTOR] Chant of Fortress (source) dam", dam) end end - end + end -- Psychic Projection if src.attr and src:attr("is_psychic_projection") and not game.zone.is_dream_scape then @@ -408,7 +408,7 @@ setDefaultProjector(function(src, x, y, type, dam, tmp, no_martyr) end end - if src.necrotic_minion_be_nice and src.summoner == target then + if src.necrotic_minion_be_nice and src.summoner == target then dam = dam * (1 - src.necrotic_minion_be_nice) end @@ -649,7 +649,7 @@ newDamageType{ else local old = src.fire_convert_to src.fire_convert_to = nil - dam = DamageType:get(old[1]).projector(src, x, y, old[1], dam * old[2] / 100) + + dam = DamageType:get(old[1]).projector(src, x, y, old[1], dam * old[2] / 100) + DamageType:get(type).projector(src, x, y, type, dam * (100 - old[2]) / 100) src.fire_convert_to = old return dam @@ -803,7 +803,7 @@ newDamageType{ end local target = game.level.map(x, y, Map.ACTOR) if target then - local energyDrain = (game.energy_to_act * 0.2) + local energyDrain = (game.energy_to_act * 0.2) target.energy.value = target.energy.value - energyDrain end end, @@ -1731,8 +1731,19 @@ newDamageType{ -- Log entries are pretty limited currently because it can be quite spammy with the default messages already newDamageType{ name = "item mind gloom", type = "ITEM_MIND_GLOOM", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to cause #YELLOW#random insanity#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to cause #YELLOW#random insanity#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1762,8 +1773,19 @@ newDamageType{ newDamageType{ name = "item darkness numbing", type = "ITEM_DARKNESS_NUMBING", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to inflict #GREY#damage reduction#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to inflict #GREY#damage reduction#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1777,18 +1799,29 @@ newDamageType{ newDamageType{ name = "item temporal energize", type = "ITEM_TEMPORAL_ENERGIZE", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to gain #LIGHT_STEEL_BLUE#%d%% of a turn#LAST#"):format(dam, 10) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to gain #LIGHT_STEEL_BLUE#10%% of a turn#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) if target and src and src.name and rng.percent(dam) then - if src.turn_procs and src.turn_procs.item_temporal_energize and src.turn_procs.item_temporal_energize > 3 then + if src.turn_procs and src.turn_procs.item_temporal_energize and src.turn_procs.item_temporal_energize > 3 then game.logSeen(src, "#LIGHT_STEEL_BLUE#%s can't gain any more energy this turn! ", src.name:capitalize()) return end - local energy = (game.energy_to_act * 0.1) + local energy = (game.energy_to_act * 0.1) src.energy.value = src.energy.value + energy --game.logSeen(target, "Time seems to bend and quicken energizing %s!", src.name:capitalize()) @@ -1799,8 +1832,19 @@ newDamageType{ newDamageType{ name = "item acid corrode", type = "ITEM_ACID_CORRODE", text_color = "#GREEN#", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to #GREEN#corrode armor#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to #GREEN#corrode armor#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1814,8 +1858,19 @@ newDamageType{ newDamageType{ name = "item light blind", type = "ITEM_LIGHT_BLIND", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to #YELLOW#blind#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to #YELLOW#blind#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1832,8 +1887,19 @@ newDamageType{ newDamageType{ name = "item lightning daze", type = "ITEM_LIGHTNING_DAZE", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to #ROYAL_BLUE#daze#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to #ROYAL_BLUE#daze#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1848,11 +1914,22 @@ newDamageType{ end end, } - + newDamageType{ name = "item blight disease", type = "ITEM_BLIGHT_DISEASE", text_color = "#DARK_GREEN#", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to #DARK_GREEN#disease#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to #DARK_GREEN#disease#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1866,8 +1943,19 @@ newDamageType{ newDamageType{ name = "item manaburn arcane", type = "ITEM_ANTIMAGIC_MANABURN", text_color = "#PURPLE#", - tdesc = function(dam) - return ("#DARK_ORCHID#%d arcane resource#LAST# burn"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d#LAST#)"):format(diff) + end + end + return ("* #DARK_ORCHID#%d arcane resource#LAST# burn%s") + :format(dam or 0, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1897,8 +1985,19 @@ newDamageType{ newDamageType{ name = "item nature slow", type = "ITEM_NATURE_SLOW", text_color = "#LIGHT_GREEN#", - tdesc = function(dam) - return ("Slows global speed by #LIGHT_GREEN#%d%%#LAST#"):format(dam) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* Slows global speed by #LIGHT_GREEN#%d%%#LAST#%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -1911,8 +2010,19 @@ newDamageType{ -- Reduces all offensive powers by 20% newDamageType{ name = "item antimagic scouring", type = "ITEM_ANTIMAGIC_SCOURING", text_color = "#ORCHID#", - tdesc = function(dam) - return ("#LIGHT_GREEN#%d%%#LAST# chance to #ORCHID#reduce powers#LAST# by %d%%"):format(dam, 20) + tdesc = function(dam, oldDam) + parens = "" + dam = dam or 0 + if oldDam then + diff = dam - oldDam + if diff > 0 then + parens = (" (#LIGHT_GREEN#+%d%%#LAST#)"):format(diff) + elseif diff < 0 then + parens = (" (#RED#%d%%#LAST#)"):format(diff) + end + end + return ("* #LIGHT_GREEN#%d%%#LAST# chance to #ORCHID#reduce powers#LAST# by %d%%%s") + :format(dam, parens) end, projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) @@ -2118,17 +2228,17 @@ newDamageType{ projector = function(src, x, y, type, dam) local target = game.level.map(x, y, Map.ACTOR) if target and not target:attr("undead") then - + target:setEffect(target.EFF_EMPOWERED_HEALING, 1, {power=(dam/200)}) if dam >= 100 then target:attr("allow_on_heal", 1) end target:heal(dam, src) - + -- If the target is shielded already then add to the shield power, else add a shield local shield_power = dam * util.bound((target.healing_factor or 1), 0, 2.5) - if not target:hasEffect(target.EFF_DAMAGE_SHIELD) then + if not target:hasEffect(target.EFF_DAMAGE_SHIELD) then target:setEffect(target.EFF_DAMAGE_SHIELD, 2, {power=shield_power}) else - -- Shields can't usually merge, so change the parameters manually + -- Shields can't usually merge, so change the parameters manually local shield = target:hasEffect(target.EFF_DAMAGE_SHIELD) shield.power = shield.power + shield_power target.damage_shield_absorb = target.damage_shield_absorb + shield_power @@ -2162,8 +2272,8 @@ newDamageType{ if dam >= 100 then src:attr("allow_on_heal", 1) end src:heal(dam / 2, src) if dam >= 100 then src:attr("allow_on_heal", -1) end - - + + end end, } @@ -3083,7 +3193,7 @@ newDamageType{ local target = game.level.map(x, y, Map.ACTOR) if target then local realdam = DamageType:get(DamageType.SLIME).projector(src, x, y, DamageType.SLIME, {dam=dam.dam, power=0.30}) - + if dam.nb > 0 then dam.done = dam.done or {} dam.done[target.uid] = true @@ -3103,7 +3213,7 @@ newDamageType{ end end return realdam - end + end end, }