-- $Id$ -- ToME - Tales of Middle-Earth -- Copyright © 2012-2017 Scott Bigham -- -- 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 . -- -- Scott Bigham "Zizzo" -- dsb-tome@killerbunnies.org local Object = require 'mod.class.Object' local Ego = require 'mod.class.Ego' local ActorTalents = require 'engine.interface.ActorTalents' local Stats = require 'mod.class.interface.ActorStats' local DamageType = require 'engine.DamageType' -- A Sneaky Hack(TM) to call resolvers.generic() in the __resolve_last -- phase of Entity:resolve(). function resolvers.generic_last(fct) return {__resolver='generic', __resolve_last=true, fct} end -- Populate a store "trap" Entity with a store object. function resolvers.store(def, type) return { __resolver='store', def, type or 'store' } end function resolvers.calc.store(t, e) e.block_move = function(self, x, y, who, act, couldpass) if who and who.player and act then local name = self.name if self.store.owner then name = ('%s [%s (%d)]'):format(name, self.store.owner.name, self.store.owner.max_cost) end self.store:loadup(game.level, game.zone) self.store:interact(who, name) end return true end e.store = game:getStore(t[1], t[2]) e.display = e.store.display or e.display e.color_r = e.store.color_r or e.color_r e.color_g = e.store.color_g or e.color_g e.color_b = e.store.color_b or e.color_b e.image = e.store.image or e.image e.all_know = true -- Delete the origin field return nil end -- Populated a store with an owner. function resolvers.store_owner(candidates) return { __resolver='store_owner', candidates } end function resolvers.calc.store_owner(t, e) -- If the resolver option is the name of a store, use its owner candidate -- list out of its resolvers.store_owner() as our candidates. local cands = t[1] if _G.type(cands) == 'string' then local Store = require 'mod.class.Store' local store = Store.stores_def[cands] cands = store.owner[1] end return game:getStoreOwner(rng.table(cands)) end function resolvers.building_chat(name, storefront) return { __resolver='building_chat', name, storefront } end function resolvers.calc.building_chat(t, e) e.chat = t[1] if t[2] then -- If a store name is specified, copy display data from it. We don't -- use Game:getStore() here, because that creates and resolve()'s a -- clone, which is more than we need. local Store = require 'mod.class.Store' local store = Store.stores_def[t[2]] if store then e.display = store.display or e.display e.color_r = store.color_r or e.color_r e.color_g = store.color_g or e.color_g e.color_b = store.color_b or e.color_b e.image = store.image or e.image e.all_know = true end end e.block_move = function(self, x, y, who, act, couldpass) if who and who.player and act then local Chat = require 'engine.Chat' local chat = Chat.new(self.chat, self, who, {npc=self, player=who}) chat:invoke() end return true end -- Delete the origin field return nil end -- Populate player's birth equipment. function resolvers.birth_equip(t) return { __resolver='birth_equip', __resolve_last=true, t } end function resolvers.calc.birth_equip(t, e) for _, filter in ipairs(t[1]) do -- Have to get a new ref to this every time, since Object:resolve() -- replaces it out from under us. local RF = Object.resolve_flags -- Slight Hack(TM): No getting magical starting equipment... >;) RF.force_power = 0 if filter.name == 'WOODEN_TORCH' then -- Slight Hack(TM): Set a reasonable fuel amount. [cf src/birth.c:2421] RF.force_lite_fuel = rng.range(3, 7)*500 end -- And any other resolve flags the object may require. for k, v in pairs(filter.resolve or {}) do RF[k] = v end local o = game.zone:makeEntityByName(game.level, 'object', filter.name, false) if o then o:identify('full') if filter.qty then local n if type(filter.qty) == 'table' then n = rng.dice(filter.qty[1], filter.qty[2]) + (filter.qty[3] or 0) elseif type(filter.qty) == 'number' then n = filter.qty end o:setNumber(n) end o.found = { type='birth' } local ro = not filter.no_auto_equip and e:wearObject(o, true, false) if ro then if type(ro) == 'table' then e:addObject(e.INVEN_INVEN, ro) end elseif not ro then e:addObject(e.INVEN_INVEN, o) end else print(('[BIRTH] ??? no objects match %s'):format(filter.name)) end end e:sortInven() -- Delete the origin field return nil end -- Apply mana multipliers from the birth descriptors. function resolvers.birth_mana_mult(mult) return { __resolver='birth_mana_mult', mult } end function resolvers.calc.birth_mana_mult(t, e) e.base_mana_mult = e.base_mana_mult * t[1]; -- Delete the origin field return nil end -- Initialize player age/height/weight based on birth descriptor data. function resolvers.birth_ahw() return { __resolver='birth_ahw' } end function resolvers.calc.birth_ahw(t, e) -- cf. get_height_weight() and get_ahw() in src/birth.c local sex = e.descriptor.sex e.age = e.ahw.age.base + rng.range(1, e.ahw.age.mod) e.height = rng.normal(e.ahw.height[sex].mean, e.ahw.height[sex].std) e.weight = rng.normal(e.ahw.weight[sex].mean, e.ahw.weight[sex].std) e.height = math.max(1, e.height) e.weight = math.max(1, e.weight) -- Don't need this anymore. e.ahw = nil -- Delete the origin field. return nil end -- Initialize random player luck with adjustments from birth descriptors. function resolvers.birth_luck() return { __resolver='birth_luck' } end function resolvers.calc.birth_luck(t, e) e.luck = rng.range(-5, 5) + (e.birth_adj_luck or 0) -- Clean this up behind us. e.birth_adj_luck = nil -- Delete the origin field return nil end -- Apply stat adjustments from the birth descriptors. The 'maximize mode' -- user setting is applied here. function resolvers.birth_stat_apply() return { __resolver='birth_stat_apply', __resolve_last=true } end function resolvers.calc.birth_stat_apply(t, e) for id, delta in pairs(e.adj_stat or {}) do if game.state.birth.maximize then local v = e:getStat(id, { raw='birth_adj' }) e:setStat(id, { birth_adj=v+delta }) else local v = e:getStat(id, { raw='base' }) e:setStat(id, { base=v+delta*10, cur=v+delta*10 }) end end -- Clean this up behind us. e.adj_stat = nil -- Delete the origin field return nil end -- Apply skill adjustments from the birth descriptors. function resolvers.birth_skill_adj(t) return { __resolver='birth_skill_adj', t } end function resolvers.calc.birth_skill_adj(t, e) for key, adj in pairs(t[1]) do local sk = e.skills[key] if not sk then print('[RESOLVER] ??? bad skill '..key) else sk.value = sk.value + (adj.base_adj or 0) sk.mod = sk.mod + (adj.mod_adj or 0) if adj.base_set then sk.value = adj.base_set end if adj.mod_set then sk.mod = adj.mod_set end end end -- Delete the origin field return nil end -- Since the preceding bypasses :setSkillValue() and thus doesn't trigger -- the :on_incrase() callback, we do that here. function resolvers.birth_skill_finalize() return { __resolver='birth_skill_finalize', __resolve_last=true } end function resolvers.calc.birth_skill_finalize(t, e) for key, sk in pairs(e.skills) do if sk.value > 0 then local def = e.skills_def[key] if def.on_increase then def:on_increase(e, 0, sk.value) end end end -- Delete the origin field return nil end -- Merge in the gain-property/flag-at-level tables from the birth -- descriptors. function resolvers.birth_gain_at_lev(t) return { __resolver='birth_gain_at_lev', t } end function resolvers.calc.birth_gain_at_lev(t, e) e.gain_at_level = e.gain_at_level or {} for lev, spec in pairs(t[1]) do e.gain_at_level[lev] = e.gain_at_level[lev] or {} table.merge(e.gain_at_level[lev], spec, true) end -- Delete the origin field return nil end -- Precompute all our level-up hit point gains at birth. function resolvers.birth_hp_precalc() return { __resolver='birth_hp_precalc', __resolve_last=true } end function resolvers.calc.birth_hp_precalc(t, e) -- Constrain the final total base HP at level 50. local min_l50_hp = math.floor((e.max_level * (e.hit_die - 1) * 3)/8) local max_l50_hp = math.floor((e.max_level * (e.hit_die - 1) * 5)/8) local hp = { [1] = e.hit_die } while true do for lev = 2, 50 do hp[lev] = hp[lev-1] + rng.range(1, e.hit_die) end local hp_l50 = hp[e.max_level] if hp_l50 >= min_l50_hp and hp_l50 <= max_l50_hp then break end end e.base_hp_by_lev = hp -- Delete the origin field return nil end -- Adjust starting piety according to whether we have a god and whether -- we're a priest. function resolvers.birth_piety(has_god) return { __resolver='birth_piety', has_god } end function resolvers.calc.birth_piety(t, e) local has_god = t[1] e.piety_regen = 0 if not has_god then -- No god? No piety. e.piety = 0 e.last_piety_regen = nil elseif e.base_flags and e.base_flags.GOD_FRIEND then -- Priests get higher starting piety. [We check base_flags{} instead -- of flags{} because the former haven't yet been copied into the -- latter by recalcEverything().] e.piety = 200 e.last_piety_regen = 0 else -- Normal piety for a god follower. e.piety = 100 e.last_piety_regen = 0 end end -- Adjust faction reactions between players/pets and Valar-related factions -- according to what god (if any) we follow. function resolvers.birth_valar_faction(eru, yavanna) return { __resolver='birth_valar_faction', eru, yavanna } end function resolvers.calc.birth_valar_faction(t, e) local eru, yavanna = t[1], t[2] local Faction = require 'engine.Faction' Faction:setFactionReaction('players', 'eru_manwe', eru, true) Faction:setFactionReaction('pets', 'eru_manwe', eru, true) Faction:setFactionReaction('players', 'yavanna', yavanna, true) Faction:setFactionReaction('pets', 'yavanna', yavanna, true) end -- Populate the monster's starting inventory. function resolvers.mon_inven(drops) return { __resolver='mon_inven', drops } end function resolvers.calc.mon_inven(t, e) if e.populateInitialInventory then e:populateInitialInventory(t[1]) end -- Populate some info for tooltips and monster memory. local max_amts = { ['60%'] = 1, ['90%'] = 1, ['1d2'] = 2, ['2d2'] = 4, ['3d2'] = 6, ['4d2'] = 8, } local copy_flags = {'good', 'great', 'randart'} e.drop_desc = { items=0, gold=0 } for _, f in ipairs(copy_flags) do e.drop_desc[f] = t[1][f] end local max_drop = 0 for _, f in ipairs(t[1]) do max_drop = max_drop + (max_amts[f] or 0) end if not t[1].only_item then e.drop_desc.items = max_drop end if not t[1].only_gold then e.drop_desc.gold = max_drop end -- Delete the origin field return nil end -- Populate the monster's initial sieep state. function resolvers.mon_sleep() return { __resolver='mon_sleep' } end function resolvers.calc.mon_sleep(t, e) if e.alertness > 0 then return e.alertness*2 + rng.range(1, e.alertness*10) else return 0 end end -- Populate the monster's initial global speed. function resolvers.mon_global_speed() return { __resolver='mon_global_speed' } end function resolvers.calc.mon_global_speed(t, e) e:computeGlobalSpeed() -- Keep current and initial speed around for later hasting. e.base_speed = e.speed return e.speed end -- Set up replace_display for mimics from their inventory. We assume this -- is only present on monsters with the MIMIC flag. function resolvers.mon_mimic() return { __resolver='mon_mimic' } end function resolvers.calc.mon_mimic(t, e) local inven = e:getInven('INVEN') if inven and #inven > 0 then e:replaceDisplay('mimic', inven[1]) end -- Delete the origin field return nil end -- Initialize a junkart. function resolvers.junkart() return { __resolver='junkart' } end function resolvers.calc.junkart(t, e) -- Pick an unused junkart name. local names_def = require('mod.class.interface.ObjectData').junkart_names local used_names = game.state.junkarts_seen if #used_names >= #names_def then -- Oops, used all the names! Start over again with a "(v2)" suffix. game.state.junkarts_seen = { wrap = used_names.wrap+1 } used_names = game.state.junkarts_seen end local used = {} for _, i in ipairs(used_names) do used[i] = true end local avail = {} for i = 1, #names_def do if not used[i] then avail[#avail+1]=i end end local j = rng.table(avail) used_names[#used_names+1] = j e.display_name = names_def[j].name e.unid_name = names_def[j].unid_name if used_names.wrap > 0 then e.display_name = e.display_name .. (' (v%d)'):format(used_names.wrap+1) end -- Assign a random color. local color = rng.table{ colors.C_w, colors.C_s, colors.C_o, colors.C_r, colors.C_g, colors.C_b, colors.C_u, colors.C_D, colors.C_W, colors.C_v, colors.C_y, colors.C_R, colors.C_G, colors.C_B, colors.C_U } for k, v in pairs(color) do e['color_'..k] = v end -- Assign a random cost. e.cost = math.floor(math.max(0, rng.normal(0, 250))) -- Set the sval for sorting purposes. e.sval = used_names.wrap * #names_def + #used_names -- Assign a random junkart power. It's okay to re-use these, as there -- are fewer powers than names. local pow = rng.table(Object.junkart_acts) if pow.cost then e.use_power = { name = pow.desc, use = pow.use, power = pow.cost } e.power = pow.cost e.max_power = pow.cost e.power_regen = 1 else e.use_simple = { name = pow.desc, use = pow.use } end -- Delete the origin field return nil end -- Assign a flavor to flavored objects that haven't had a flavor assigned -- to them, and adjust a flavored object's color and tile image for its -- flavor. function resolvers.flavored() return { __resolver='flavored' } end function resolvers.calc.flavored(t, e) local fl_def = e:getFlavor() if fl_def then local used = game.state.flavors_assigned[e.type][e.subtype] if not used[e.name] then used[e.name] = fl_def.pop_flavor(e.type, e.subtype) end local color = used[e.name][2] for k, v in pairs(color) do e['color_'..k] = v end e.image = used[e.name][3] end end -- Initialize a weapon, launcher, ammo, boomerang or trapkit. Some of -- these will have additional resolvers, detailed below. function resolvers.weapon() return { __resolver='weapon' } end function resolvers.calc.weapon(t, e) -- Don't modify artifacts. if e.artifact_name then return nil end local RF = Object.resolve_flags local abs_pow = math.abs(RF.power) if RF.force_ego or abs_pow > 1 then -- Small chance of a randart. if RF.power > 1 and rng.chance(30) then obj_util.convert_to_randart(e) return nil -- Delete the origin field end Ego:tryAddEgos(e, RF.power > 0) -- Recompute this, as it might have changed abs_pow = math.abs(RF.power) end local th = abs_pow > 0 and rng.range(1, 5) + obj_util.m_bonus(5, RF.level) or 0 th = th + (abs_pow > 1 and obj_util.m_bonus(10, RF.level) or 0) local td = abs_pow > 0 and rng.range(1, 5) + obj_util.m_bonus(5, RF.level) or 0 td = td + (abs_pow > 1 and obj_util.m_bonus(10, RF.level) or 0) e.to_h = (e.to_h or 0) + th * (RF.power < 0 and -1 or 1) e.to_d = (e.to_d or 0) + td * (RF.power < 0 and -1 or 1) if e.to_h + e.to_d < 0 then e.cursed = true end -- Delete the origin field return nil end -- Special initialization for ammo. -- NOTE: This may run before or after resolvers.weapon(). function resolvers.ammo() return { __resolver='ammo' } end function resolvers.calc.ammo(t, e) -- Don't modify artifacts. if e.artifact_name then return nil end local RF = Object.resolve_flags if RF.power == 1 and not e.ego_names and not RF.force_ego and rng.percent(30) then local dts = { DamageType.ELEC, DamageType.POISON, DamageType.ACID, DamageType.COLD, DamageType.FIRE, DamageType.PLASMA, DamageType.LITE, DamageType.DARK, DamageType.SHARDS, DamageType.SOUND, DamageType.CONFUSION, DamageType.FORCE, DamageType.INERTIA, DamageType.MANA, DamageType.METEOR, DamageType.ICE, DamageType.CHAOS, DamageType.NETHER, DamageType.NEXUS, DamageType.TIME, DamageType.GRAVITY, DamageType.KILL_WALL, DamageType.AWAY_ALL, DamageType.TURN_ALL, DamageType.NUKE, DamageType.STUN, DamageType.DISINTEGRATE, } e.explode_dam_type = rng.table(dts) e.explode_r = math.min(e.sval+2, 4) e.explode_mult = e.sval == 0 and 0.5 or e.sval == 1 and 1 or 2 end -- Delete the origin field return nil end -- Special initialization for trapkits. -- NOTE: This may run before or after resolvers.weapon(). function resolvers.trapkit() return { __resolver='trapkit' } end function resolvers.calc.trapkit(t, e) -- Don't modify artifacts. if e.artifact_name then return nil end local RF = Object.resolve_flags local abs_pow = math.abs(RF.power) e.to_a = (e.to_a or 0) + (RF.power > 0 and 1 or -1) * ((abs_pow > 0 and rng.range(1, 5) or 0) + (abs_pow > 1 and rng.range(1, 5) or 0)) -- Delete the origin field return nil end -- Initialize a light source. function resolvers.light() return { __resolver='light' } end function resolvers.calc.light(t, e) local RF = Object.resolve_flags -- Slight Hack(TM): Torches and lanterns generated in stores are always -- fully fueled. if e.lite_fuel and not RF.store_pop then e.lite_fuel = RF.force_lite_fuel or rng.range(1, e.lite_fuel) end -- Delete the origin field return nil end -- Initialize an armor piece. function resolvers.armor() return { __resolver='armor' } end function resolvers.calc.armor(t, e) -- Don't modify artifacts. if e.artifact_name then return nil end local RF = Object.resolve_flags local abs_pow = math.abs(RF.power) if RF.force_ego or abs_pow > 1 then -- Small chance of a randart. if RF.power > 1 and rng.chance(20) then obj_util.convert_to_randart(e) return nil -- Delete the origin field end Ego:tryAddEgos(e, RF.power > 0) -- Recompute this, as it might have changed abs_pow = math.abs(RF.power) end e.to_a = (e.to_a or 0) + (RF.power > 0 and 1 or -1) * ((abs_pow > 0 and rng.range(1, 5) + obj_util.m_bonus(5, RF.level) or 0) + (abs_pow > 1 and obj_util.m_bonus(10, RF.level) or 0)) if e.to_a < 0 then e.cursed = true end -- Delete the origin field return nil end -- Adds one or more random resists to a non-body-armor dragon armor piece. -- cf. dragon_resist() in src/object2.c function resolvers.dragon_resist() return { __resolver='dragon_resist' } end function resolvers.calc.dragon_resist(t, e) repeat local idx = rng.chance(4) and rng.range(5, 18) or rng.range(17, 38) obj_util.random_resistance(e, idx) until rng.chance(2) -- Delete the origin field return nil end -- If the requested object power is high enough, attempt to add one or more -- egos to an object. -- Not needed for weapons and armor; their resolvers already do this. function resolvers.ego() return { __resolver='ego' } end function resolvers.calc.ego(t, e) -- Don't modify artifacts. if e.artifact_name then return nil end local RF = Object.resolve_flags local abs_pow = math.abs(RF.power) if RF.force_ego or abs_pow > 1 then -- Small chance of a randart if RF.power > 1 and obj_util.randartable_kind(e) and rng.chance(20) then obj_util.convert_to_randart(e) return nil -- Delete the origin field end Ego:tryAddEgos(e, RF.power > 0) end -- Delete the origin field return nil end -- Adds a random stat sustain to an ego object. function resolvers.ego_sustain() return { __resolver='ego_sustain' } end function resolvers.calc.ego_sustain(t, e) e.convey_flags = e.convey_flags or {} e.convey_flags['SUST_'..rng.table(Stats.stats)] = true -- Delete the origin field return nil end -- Adds a random "old" resistance to an ego object. function resolvers.ego_old_resist() return { __resolver='ego_old_resist' } end function resolvers.calc.ego_old_resist(t, e) local resist = rng.table({ DamageType.BLIND, DamageType.CONFUSION, DamageType.SOUND, DamageType.SHARDS, DamageType.NETHER, DamageType.NEXUS, DamageType.CHAOS, DamageType.DISENCHANT, DamageType.POISON, DamageType.DARK, DamageType.LITE, }) e.convey_resist_dam = e.convey_resist_dam or {} e.convey_resist_dam[resist] = true -- Delete the origin field return nil end -- Adds a random ability flag to an ego object. function resolvers.ego_abil_flag() return { __resolver='ego_abil_flag' } end function resolvers.calc.ego_abil_flag(t, e) obj_util.random_ability(e) -- Delete the origin field return nil end -- Adds a random pval flag to an ego object. function resolvers.ego_pval_flag() return { __resolver='ego_pval_flag' } end function resolvers.calc.ego_pval_flag(t, e) local flag = rng.table({ 'skill_stealth', 'skill_searching', 'infravision', 'adj_digging', 'speed', 'extra_blows', }) e.pval_flags = e.pval_flags or {} e.pval_flags[flag] = true -- Delete the origin field return nil end -- Adds a random stat flag to an ego object. function resolvers.ego_stat_flag() return { __resolver='ego_stat_flag' } end function resolvers.calc.ego_stat_flag(t, e) e.stat_flags = e.stat_flags or {} e.stat_flags[rng.table(Stats.stats)] = true -- Delete the origin field return nil end -- Adds a random stat flag and the corresponding stat sustain flag to an -- ego object. function resolvers.ego_stat_and_sustain() return { __resolver='ego_stat_and_sustain' } end function resolvers.calc.ego_stat_and_sustain(t, e) local stat = rng.table(Stats.stats) e.stat_flags = e.stat_flags or {} e.stat_flags[stat] = true e.convey_flags = e.convey_flags or {} e.convey_flags['SUST_'..stat] = true -- Delete the origin field return nil end -- Adds a random "elemental" resist (acid/ltng/fire/cold/poison) to an ego -- object. function resolvers.ego_elem_resist() return { __resolver='ego_elem_resist' } end function resolvers.calc.ego_elem_resist(t, e) obj_util.random_resistance(e, rng.range(5, 18)) -- Delete the origin field return nil end -- Adds a random "low" resist (acid/ltng/fire/cold) to an ego object. function resolvers.ego_low_resist() return { __resolver='ego_low_resist' } end function resolvers.calc.ego_low_resist(t, e) obj_util.random_resistance(e, rng.range(5, 16)) -- Delete the origin field return nil end -- Adds a random "high" resist to an ego object. function resolvers.ego_high_resist() return { __resolver='ego_high_resist' } end function resolvers.calc.ego_high_resist(t, e) obj_util.random_resistance(e, rng.range(17, 38)) -- Delete the origin field return nil end -- Adds a random resist ("low" or "high") to an ego object. function resolvers.ego_any_resist() return { __resolver='ego_any_resist' } end function resolvers.calc.ego_any_resist(t, e) obj_util.random_resistance(e, rng.range(5, 38)) -- Delete the origin field return nil end -- Apply the 'of Slaying' ego to a weapon. function resolvers.ego_slaying() return { __resolver='ego_slaying' } end function resolvers.calc.ego_slaying(t, e) if e.combat then if rng.chance(3) then -- 1/3 chance of double dice e.combat[1] = e.combat[1] * 2 else -- Otherwise, bump dice and sides up a few times. repeat e.combat[1] = e.combat[1] + 1 until not rng.chance(e.combat[1]) repeat e.combat[2] = e.combat[2] + 1 until not rng.chance(e.combat[2]) end -- 20% chance of poison brand if rng.chance(5) then e.flags = e.flags or {} e.flags.BRAND_POIS = true end -- For swords, 1/3 chance of vorpal if e.tval == 23 and rng.chance(3) then e.flags = e.flags or {} e.flags.VORPAL = true end end -- Delete the origin field return nil end -- Boost damage dice or sides by 1. function resolvers.ego_adjust_dam_dice(n) return { __resolver='ego_adjust_dam_dice', n } end function resolvers.calc.ego_adjust_dam_dice(t, e) if e.combat then e.combat[t[1]] = e.combat[t[1]] + 1 end -- Delete the origin field return nil end -- Boost specified field by a level-dependent amount. function resolvers.ego_adjust_field(field, n) return { __resolver='ego_adjust_field', field, n } end function resolvers.calc.ego_adjust_field(t, e) local field, n = t[1], t[2] if n > 1 then n = obj_util.m_bonus(n, Object.resolve_flags.level) end e[field] = (e[field] or 0) + n -- Delete the origin field return nil end -- Adds a random immunity to an ego object. function resolvers.ego_immunity() return { __resolver='ego_immunity' } end function resolvers.calc.ego_immunity(t, e) local imm = rng.table({ DamageType.FIRE, DamageType.ACID, DamageType.ELEC, DamageType.COLD, }) e.convey_immune_dam = e.convey_immune_dam or {} e.convey_immune_dam[imm] = true e.ignore_dam = e.ignore_dam or {} e.ignore_dam[imm] = true -- Delete the origin field return nil end -- Set up rods. function resolvers.rod() return { __resolver='rod' } end function resolvers.calc.rod(t, e) -- Start us out fully charged. e.power = e.max_power return nil end -- Apply certain rod ego flags. function resolvers.ego_rod() return { __resolver='ego_rod' } end function resolvers.calc.ego_rod(t, e) if e.flags and e.flags.CAPACITY then e.max_power = e.max_power and e.max_power * 2 -- Increase starting power to match. e.power = e.max_power end if e.flags and e.flags.CHARGING then e.power_regen = e.power_regen and e.power_regen * 2 end if e.flags and e.flags.FAST_CAST then e.use_speed = 0.5 end -- CHEAPNESS we'll apply when adding a rod tip -- EASY_USE we'll apply when activating the rod -- Delete the origin field return nil end -- Select a spell and set up base/max levels for wands and staffs. -- [cf. a_m_aux_4() in src/object2.c] function resolvers.wand_staff() return { __resolver='wand_staff' } end function resolvers.calc.wand_staff(t, e) local RF = Object.resolve_flags if not e.use_power.spell then -- Select a suitable spell [cf. get_random_stick() in lib/core/s_aux.lua] local spells = {} local stick_type = e.type .. '/' .. e.subtype -- Collect spells that can be put on us. for tid, t in pairs(ActorTalents.talents_def) do if t.stick and t.stick.type == stick_type then spells[#spells+1] = t end end -- Now check vs level and rarity. for _ = 1, 1000 do local t = rng.table(spells) if rng.range(1, t.skill_level*3) <= RF.level and rng.percent(100 - t.stick.rarity) then e.use_power.spell = t.id break end end if not e.use_power.spell then -- Emergency fallback e.use_power.spell = e.subtype == 'wand' and ActorTalents.T_MANATHRUST or ActorTalents.T_GLOBE_LIGHT end end -- Set up base level and max level. [cf. get_stick_base_level() and -- get_stick_max_level() in lib/core/s_aux.lua] local stick_spec = ActorTalents.talents_def[e.use_power.spell] and ActorTalents.talents_def[e.use_power.spell].stick if stick_spec then local min, max = stick_spec.base_level[1], stick_spec.base_level[2] -- The basic idea is to have a max possible level of half the dungeon -- level. local range = math.min(max - min, math.floor(RF.level/2)) -- With a bit of randomness. e.use_power.base_level = min + obj_util.m_bonus(range, RF.level) -- Repeat for max_level min, max = stick_spec.max_level[1], stick_spec.max_level[2] range = math.min(max - min, math.floor(RF.level/2)) e.use_power.max_level = min + obj_util.m_bonus(range, RF.level) -- And add charges. [cf. get_stick_charges() in lib/core/s_aux.lua] e.power = stick_spec.charge[1] + rng.range(1, stick_spec.charge[2]) end end -- Mention the creation of an object if the player has precognition. function resolvers.precog_mention() return { __resolver='precog_mention' } end function resolvers.calc.precog_mention(t, e) if game.player and game.player:has('PRECOGNITION') then game.log('[precognition] ' .. e:getName()) end -- Delete the origin field return nil end -- Special setup for artifacts. function resolvers.artifact() return { __resolver='artifact' } end function resolvers.calc.artifact(t, e) e.rating = (e.rating or 0) + 10 if e.cost > 50000 then e.rating = e.rating + 10 end -- Ignore everything. e.ignore_dam = { [DamageType.ACID] = true, [DamageType.ELEC] = true, [DamageType.FIRE] = true, [DamageType.COLD] = true, } -- Dodgy and Annoying Hack(TM): Dragon armor already has a -- resolvers.precog_mention(), so we don't need to repeat it here. if game.player and game.player:has('PRECOGNITION') and not e.name:find('Dragon') then game.log('[precognition] ' .. e:getName()) end if e.flags and e.flags.LEVELS then -- TODO Set up exp for "leveling" artifacts. end -- Delete the origin field return nil end -- Select a random mimicry form. function resolvers.mimicry_form(cloak_mode) return { __resolver='mimicry_form', cloak_mode } end function resolvers.calc.mimicry_form(t, e) local ObjectData = require 'mod.class.interface.ObjectData' local RF = Object.resolve_flags local cloak_mode = t[1] local t_ret = RF.force_mimic_form and ObjectData.mimicry_forms[RF.force_mimic_form] if not t_ret then for _ = 1, 1000 do local t = rng.table(ObjectData.mimicry_forms) if (cloak_mode or not t.no_potion) and t.rarity and rng.percent(100 - t.rarity) and rng.range(1, t.level*3) <= RF.level then t_ret = t break end end end t_ret = t_ret or ObjectData.mimicry_forms['Abomination'] -- Slight Hack(TM): In cloak mode, we munge the entity directly and -- return nil to clear the source field; in potion mode, we return the -- form name into its mimic_form field. if not cloak_mode then return t_ret.name end e.worn_talents = { t_ret.talent_id, [t_ret.talent_id] = { skill = { tag='MIMICRY' } } } e.display_name = t_ret.cloak_name return nil end -- Populate a spellbook with a random spell. function resolvers.spellbook() return { __resolver='spellbook' } end function resolvers.calc.spellbook(t, e) local RF = Object.resolve_flags if RF.force_spell then return { RF.force_spell } end local spell_type = RF.force_spell_type or (rng.chance(4) and 'SPIRITUALITY' or 'MAGIC') local choices = {} for tid, t in pairs(ActorTalents.talents_def) do if t.random_book and t.random_book == spell_type then choices[#choices+1] = tid end end -- Emergency fallback if #choices == 0 then return { 'T_PHASE_DOOR' } end -- Constrain by object level. [cf. get_random_spell() in lib/core/s_aux.lua] for _ = 1, 1000 do local tid = rng.table(choices) local t = ActorTalents.talents_def[tid] if rng.range(0, 3*t.skill_level - 1) < RF.level then return { tid } end end -- Emergency fallback return { 'T_PHASE_DOOR' } end -- Choose a random monster suitable for the current level and matching the -- optional extra conditions function, and attach it as a resolved entity. function resolvers.monster(extra, params) return { __resolver='monster', extra, fallback } end function resolvers.calc.monster(t, e) local RF = Object.resolve_flags if RF.force_mon then return RF.force_mon end local extra, fallback = t[1], t[2] local filter = game.zone:createLevelActorFilter(game.level, extra) local ee = game.zone:makeEntity(game.level, 'actor', filter, nil, true) if not ee and fallback then ee = game.zone:makeEntityByName(game.level, 'actor', fallback) end return ee end -- Sets up a symbiote with a random never-moving monster (or the specified -- one via Object.resolve_flags.force_mon) and associated worn_talents{}. function resolvers.symbiote() return { __resolver='symbiote' } end function resolvers.calc.symbiote(t, e) local RF = Object.resolve_flags e.symbiote = RF.force_mon if not e.symbiote then local function extra_cb(ee) return not ee.unique and ee:has('NEVER_MOVE') and not ee:has('SPECIAL_GENE') end local filter = game.zone:createLevelActorFilter(game.level, extra) e.symbiote = game.zone:makeEntity(game.level, 'actor', filter, nil, true) if not e.symbiote then e.symbiote = game.zone:makeEntityByName(game.level, 'actor', 'GREY_MOLD') end end if e.symbiote then e.worn_talents = util.body_powers(e.symbiote, 'SYMBIOSIS', 10, 25) end -- Delete the source field return nil end -- Choose a random monster suitable for the current level and matching the -- optional extra conditions function. Returns its 'define_as' field, and -- optionally adjusts the entity's weight accordingly. function resolvers.monster_id(extra, params) return { __resolver='monster_id', extra, params } end function resolvers.calc.monster_id(t, e) local RF = Object.resolve_flags local extra = t[1] local params = t[2] or {} local filter = game.zone:createLevelActorFilter(game.level, extra) local list = game.zone:computeRarities('actor', game.zone.npc_list, game.level, function(ee) return game.zone:checkFilter(ee, filter, 'actor') end) local a = RF.force_mon or (RF.force_mon_id and game.zone.npc_list[RF.force_mon_id]) or game.zone:pickEntity(list) if not a and params.fallback then a = game.zone.npc_list[params.fallback] end if not a then return nil end if params.wgt_div then local w = a.corpse_weight e.weight = w + rng.range(0, w*10 - 1)/params.wgt_div + 1 e.encumber = e.weight end if a.unique then e.unique = true end return a.define_as end -- Set up the money value of a money object. function resolvers.money_value() return { __resolver='money_value' } end function resolvers.calc.money_value(t, e) return e.cost + 8*rng.range(1, e.cost) + rng.range(1, 8) end -- Set up a chest. -- [cf. place_trap_object() in src/traps.c and a_m_aux_4() in src/object2.c] function resolvers.chest(n_obj, small) return { __resolver='chest', n_obj, small } end function resolvers.calc.chest(t, e) local n_obj, small = t[1], t[2] if game.zone:level_adjust_level(game.level, 'object') <= 1 then -- Apparently all chests on level 1 are untrapped and empty. e.chest = { n_obj=0 } e:identify(true) else local tt = game.zone:makeEntity(game.level, 'trap', trap_util.chest_filter) if tt then tt.on_chest = e tt.on_disarm = function(self, _, _, _) self.on_chest.chest.trap = nil end e.chest = { trap=tt, n_obj=n_obj, small=small } else e.chest = { n_obj=0 } end end return nil end