-- ToME - Tales of Maj'Eyal -- Copyright (C) 2009, 2010, 2011 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 . -- -- Nicolas Casalini "DarkGod" -- darkgod@te4.org require "engine.class" require "engine.Actor" require "engine.Autolevel" require "engine.interface.ActorInventory" require "engine.interface.ActorTemporaryEffects" require "engine.interface.ActorLife" require "engine.interface.ActorProject" require "engine.interface.ActorLevel" require "engine.interface.ActorStats" require "engine.interface.ActorTalents" require "engine.interface.ActorResource" require "engine.interface.BloodyDeath" require "engine.interface.ActorFOV" require "mod.class.interface.ActorPartyQuest" require "mod.class.interface.Combat" require "mod.class.interface.Archery" require "mod.class.interface.ActorInscriptions" local Faction = require "engine.Faction" local Dialog = require "engine.ui.Dialog" local Map = require "engine.Map" local DamageType = require "engine.DamageType" module(..., package.seeall, class.inherit( -- a ToME actor is a complex beast it uses may interfaces engine.Actor, engine.interface.ActorInventory, engine.interface.ActorTemporaryEffects, engine.interface.ActorLife, engine.interface.ActorProject, engine.interface.ActorLevel, engine.interface.ActorStats, engine.interface.ActorTalents, engine.interface.ActorResource, engine.interface.BloodyDeath, engine.interface.ActorFOV, mod.class.interface.ActorPartyQuest, mod.class.interface.ActorInscriptions, mod.class.interface.Combat, mod.class.interface.Archery )) -- Dont save the can_see_cache _M._no_save_fields.can_see_cache = true -- Use distance maps _M.__do_distance_map = true _M.__is_actor = true _M.stats_per_level = 3 -- Speeds are multiplicative, not additive _M.temporary_values_conf.global_speed = "mult0" _M.temporary_values_conf.movement_speed = "mult0" _M.temporary_values_conf.combat_physspeed = "mult0" _M.temporary_values_conf.combat_spellspeed = "mult0" function _M:init(t, no_default) -- Define some basic combat stats self.energyBase = 0 self.combat_def = 0 self.combat_armor = 0 self.combat_armor_hardiness = 0 self.combat_atk = 0 self.combat_apr = 0 self.combat_dam = 0 self.global_speed = 1 self.movement_speed = 1 self.combat_physcrit = 0 self.combat_physspeed = 1 self.combat_spellspeed = 1 self.combat_spellcrit = 0 self.combat_spellpower = 0 self.combat_mindpower = 0 self.combat_mindcrit = 0 self.combat_physresist = 0 self.combat_spellresist = 0 self.combat_mentalresist = 0 self.fatigue = 0 self.spell_cooldown_reduction = 0 self.unused_stats = self.unused_stats or 3 self.unused_talents = self.unused_talents or 2 self.unused_generics = self.unused_generics or 1 self.unused_talents_types = self.unused_talents_types or 0 t.healing_factor = t.healing_factor or 1 t.sight = t.sight or 10 t.resource_pool_refs = t.resource_pool_refs or {} t.lite = t.lite or 0 t.size_category = t.size_category or 3 t.rank = t.rank or 2 t.life_rating = t.life_rating or 10 t.mana_rating = t.mana_rating or 4 t.vim_rating = t.vim_rating or 4 t.stamina_rating = t.stamina_rating or 3 t.positive_negative_rating = t.positive_negative_rating or 3 t.psi_rating = t.psi_rating or 0 t.esp = t.esp or {} t.esp_range = t.esp_range or 10 t.talent_cd_reduction = t.talent_cd_reduction or {} t.on_melee_hit = t.on_melee_hit or {} t.melee_project = t.melee_project or {} t.ranged_project = t.ranged_project or {} t.can_pass = t.can_pass or {} t.move_project = t.move_project or {} t.can_breath = t.can_breath or {} t.ai_tactic = t.ai_tactic or {} -- Resistances t.resists = t.resists or {} t.resists_cap = t.resists_cap or { all = 100 } t.resists_pen = t.resists_pen or {} -- % Increase damage t.inc_damage = t.inc_damage or {} -- Default regen t.air_regen = t.air_regen or 3 t.mana_regen = t.mana_regen or 0.5 t.stamina_regen = t.stamina_regen or 0.3 -- Stamina regens slower than mana t.life_regen = t.life_regen or 0.25 -- Life regen real slow t.equilibrium_regen = t.equilibrium_regen or 0 -- Equilibrium does not regen t.vim_regen = t.vim_regen or 0 -- Vim does not regen t.positive_regen = t.positive_regen or -0.2 -- Positive energy slowly decays t.negative_regen = t.negative_regen or -0.2 -- Positive energy slowly decays t.paradox_regen = t.paradox_regen or 0 -- Paradox does not regen t.psi_regen = t.psi_regen or 0 -- Energy does not regen t.max_positive = t.max_positive or 50 t.max_negative = t.max_negative or 50 t.positive = t.positive or 0 t.negative = t.negative or 0 t.hate_rating = t.hate_rating or 0.2 t.hate_regen = t.hate_regen or 0 t.max_hate = t.max_hate or 10 t.absolute_max_hate = t.absolute_max_hate or 14 t.hate = t.hate or 10 t.hate_per_kill = t.hate_per_kill or 0.8 t.equilibrium = t.equilibrium or 0 t.paradox = t.paradox or 150 t.money = t.money or 0 engine.Actor.init(self, t, no_default) engine.interface.ActorInventory.init(self, t) engine.interface.ActorTemporaryEffects.init(self, t) engine.interface.ActorLife.init(self, t) engine.interface.ActorProject.init(self, t) engine.interface.ActorTalents.init(self, t) engine.interface.ActorResource.init(self, t) engine.interface.ActorStats.init(self, t) engine.interface.ActorLevel.init(self, t) engine.interface.ActorFOV.init(self, t) mod.class.interface.ActorInscriptions.init(self, t) -- Default melee barehanded damage self.combat = self.combat or { dam=1, atk=1, apr=0, physcrit=0, physspeed =1, dammod = { str=1 }, damrange=1.1, talented = "unarmed", } -- Insures we have certain values for gloves to modify self.combat.damrange = self.combat.damrange or 1.1 self.combat.physspeed = self.combat.physspeed or 1 self.combat.dammod = self.combat.dammod or {str=0.6} self.talents[self.T_ATTACK] = self.talents[self.T_ATTACK] or 1 self:resetCanSeeCache() end function _M:useEnergy(val) engine.Actor.useEnergy(self, val) -- Do not fire those talents if this is not turn's end if self:enoughEnergy() or game.zone.wilderness then return end if self:isTalentActive(self.T_KINETIC_AURA) then local t = self:getTalentFromId(self.T_KINETIC_AURA) t.do_kineticaura(self, t) end if self:isTalentActive(self.T_THERMAL_AURA) then local t = self:getTalentFromId(self.T_THERMAL_AURA) t.do_thermalaura(self, t) end if self:isTalentActive(self.T_CHARGED_AURA) then local t = self:getTalentFromId(self.T_CHARGED_AURA) t.do_chargedaura(self, t) end if self:isTalentActive(self.T_BEYOND_THE_FLESH) then local t = self:getTalentFromId(self.T_BEYOND_THE_FLESH) t.do_tkautoattack(self, t) end end function _M:actBase() self.energyBase = self.energyBase - game.energy_to_act if self:isTalentActive (self.T_DARKEST_LIGHT) and self.positive > self.negative then self:forceUseTalent(self.T_DARKEST_LIGHT, {ignore_energy=true}) game.logSeen(self, "%s's darkness can no longer hold back the light!", self.name:capitalize()) end -- Cooldown talents if not self:attr("stunned") then self:cooldownTalents() end -- Regen resources self:regenLife() if self:knowTalent(self.T_UNNATURAL_BODY) then local t = self:getTalentFromId(self.T_UNNATURAL_BODY) t.do_regenLife(self, t) end self:regenResources() -- Hate decay if self:knowTalent(self.T_HATE_POOL) and self.hate > 0 then -- hate loss speeds up as hate increases local hateChange = -math.max(0.02, 0.07 * math.pow(self.hate / 10, 1.5)) self:incHate(hateChange) end -- Compute timed effects self:timedEffects() -- Handle thunderstorm, even if the actor is stunned or incapacitated it still works if not game.zone.wilderness then if self:isTalentActive(self.T_THUNDERSTORM) then local t = self:getTalentFromId(self.T_THUNDERSTORM) t.do_storm(self, t) end if self:isTalentActive(self.T_STONE_VINES) then local t = self:getTalentFromId(self.T_STONE_VINES) t.do_vines(self, t) end if self:isTalentActive(self.T_BODY_OF_FIRE) then local t = self:getTalentFromId(self.T_BODY_OF_FIRE) t.do_fire(self, t) end if self:isTalentActive(self.T_HYMN_OF_MOONLIGHT) then local t = self:getTalentFromId(self.T_HYMN_OF_MOONLIGHT) t.do_beams(self, t) end if self:isTalentActive(self.T_BLOOD_FRENZY) then local t = self:getTalentFromId(self.T_BLOOD_FRENZY) t.do_turn(self, t) end if self:isTalentActive(self.T_TRUE_GRIT) then local t = self:getTalentFromId(self.T_TRUE_GRIT) t.do_turn(self, t) end -- this handles cursed gloom turn based effects if self:isTalentActive(self.T_GLOOM) then local t = self:getTalentFromId(self.T_GLOOM) t.do_gloom(self, t) end -- this handles cursed call shadows turn based effects if self:isTalentActive(self.T_CALL_SHADOWS) then local t = self:getTalentFromId(self.T_CALL_SHADOWS) t.do_callShadows(self, t) end -- this handles cursed deflection turn based effects if self:isTalentActive(self.T_DEFLECTION) then local t = self:getTalentFromId(self.T_DEFLECTION) t.do_act(self, t, self:isTalentActive(self.T_DEFLECTION)) end -- this handles doomed unseen force turn based effects if self.unseenForce then local t = self:getTalentFromId(self.T_UNSEEN_FORCE) t.do_unseenForce(self, t) end -- this handles doomed arcane bolts turn based effects if self.arcaneBolts then local t = self:getTalentFromId(self.T_ARCANE_BOLTS) t.do_arcaneBolts(self, t) end -- this handles Door to the Past random anomalies if self:isTalentActive(self.T_DOOR_TO_THE_PAST) then local t = self:getTalentFromId(self.T_DOOR_TO_THE_PAST) t.do_anomalyCount(self, t) end -- this handles Carbon Spike regrowth if self:isTalentActive(self.T_CARBON_SPIKES) then local t = self:getTalentFromId(self.T_CARBON_SPIKES) t.do_carbonRegrowth(self, t) end end -- Suffocate ? local air_level, air_condition = game.level.map:checkEntity(self.x, self.y, Map.TERRAIN, "air_level"), game.level.map:checkEntity(self.x, self.y, Map.TERRAIN, "air_condition") if air_level then if not air_condition or not self.can_breath[air_condition] or self.can_breath[air_condition] <= 0 then self:suffocate(-air_level, self, air_condition == "water" and "drowned to death" or nil) end end end function _M:act() if not engine.Actor.act(self) then return end self.changed = true -- If resources are too low, disable sustains if self.mana < 1 or self.stamina < 1 or self.psi < 1 then for tid, _ in pairs(self.sustain_talents) do local t = self:getTalentFromId(tid) if (t.sustain_mana and self.mana < 1) or (t.sustain_stamina and self.stamina < 1) then self:forceUseTalent(tid, {ignore_energy=true}) elseif (t.sustain_psi and self.psi < 1) and t.remove_on_zero then self:forceUseTalent(tid, {ignore_energy=true}) end end end -- Conduit talent prevents all auras from cooling down if self:isTalentActive(self.T_CONDUIT) then local auras = self:isTalentActive(self.T_CONDUIT) if auras.k_aura_on then local t_kinetic_aura = self:getTalentFromId(self.T_KINETIC_AURA) self.talents_cd[self.T_KINETIC_AURA] = t_kinetic_aura.cooldown(self, t) end if auras.t_aura_on then local t_thermal_aura = self:getTalentFromId(self.T_THERMAL_AURA) self.talents_cd[self.T_THERMAL_AURA] = t_thermal_aura.cooldown(self, t) end if auras.c_aura_on then local t_charged_aura = self:getTalentFromId(self.T_CHARGED_AURA) self.talents_cd[self.T_CHARGED_AURA] = t_charged_aura.cooldown(self, t) end end if self:attr("paralyzed") then self.paralyzed_counter = (self.paralyzed_counter or 0) + (self:attr("stun_immune") or 0) * 100 if self.paralyzed_counter < 100 then self.energy.value = 0 else -- We are saved for this turn self.paralyzed_counter = self.paralyzed_counter - 100 game.logSeen(self, "%s temporarily fights the paralyzation.", self.name:capitalize()) end end if self:attr("stoned") then self.energy.value = 0 end if self:attr("dazed") then self.energy.value = 0 end if self:attr("time_stun") then self.energy.value = 0 end if self:attr("time_prison") then self.energy.value = 0 end -- Regain natural balance? local equilibrium_level = game.level.map:checkEntity(self.x, self.y, Map.TERRAIN, "equilibrium_level") if equilibrium_level then self:incEquilibrium(equilibrium_level) end -- Do stuff to things standing in the fire game.level.map:checkEntity(self.x, self.y, Map.TERRAIN, "on_stand", self) -- Still enough energy to act ? if self.energy.value < game.energy_to_act then return false end -- Still not dead ? if self.dead then return false end -- Ok reset the seen cache self:resetCanSeeCache() if self.on_act then self:on_act() end if self.never_act then return false end if not game.zone.wilderness then self:automaticTalents() end -- Compute bonuses based on actors in FOV if self:knowTalent(self.T_MILITANT_MIND) and not self:hasEffect(self.EFF_MILITANT_MIND) then local nb_foes = 0 local act for i = 1, #self.fov.actors_dist do act = self.fov.actors_dist[i] if self:reactionToward(act) < 0 and self:canSee(act) then nb_foes = nb_foes + 1 end end if nb_foes > 1 then nb_foes = math.min(nb_foes, self:getTalentLevel(self.T_MILITANT_MIND)) self:setEffect(self.EFF_MILITANT_MIND, 4, {power=self:getTalentLevel(self.T_MILITANT_MIND) * nb_foes * 0.6}) end end -- Still enough energy to act ? if self.energy.value < game.energy_to_act then return false end return true end --- Setup minimap color for this entity -- You may overload this method to customize your minimap function _M:setupMinimapInfo(mo, map) if map.actor_player and not map.actor_player:canSee(self) then return end if self.rank > 3 then mo:minimap(0xC0, 0x00, 0xAF) return end local r = map.actor_player and map.actor_player:reactionToward(self) or -100 if r < 0 then mo:minimap(240, 0, 0) elseif r > 0 then mo:minimap(0, 240, 0) else mo:minimap(0, 0, 240) end end --- Attach or remove a display callback -- Defines particles to display function _M:defineDisplayCallback() if not self._mo then return end local ps = self:getParticlesList() local f_self = nil local f_danger = nil local f_powerful = nil local f_friend = nil local f_enemy = nil local f_neutral = nil self._mo:displayCallback(function(x, y, w, h, zoom, on_map) -- Tactical info if game.level and game.always_target then -- Tactical life info if on_map then local dh = h * 0.1 local lp = self.life / self.max_life + 0.0001 core.display.drawQuad(x + 3, y + h - dh, w - 6, dh, 129, 180, 57, 128) core.display.drawQuad(x + 3, y + h - dh, (w - 6) * lp, dh, 50, 220, 77, 255) end end -- Tactical info if game.level and game.level.map.view_faction then local map = game.level.map if on_map then if not f_self then f_self = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_self) f_powerful = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_powerful) f_danger = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_danger) f_friend = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_friend) f_enemy = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_enemy) f_neutral = game.level.map.tilesTactic:get(nil, 0,0,0, 0,0,0, map.faction_neutral) end if self.faction then local friend if not map.actor_player then friend = Faction:factionReaction(map.view_faction, self.faction) else friend = map.actor_player:reactionToward(self) end if self == map.actor_player then f_self:toScreen(x, y, w, h) elseif map:faction_danger_check(self) then if friend >= 0 then f_powerful:toScreen(x, y, w, h) else f_danger:toScreen(x, y, w, h) end elseif friend > 0 then f_friend:toScreen(x, y, w, h) elseif friend < 0 then f_enemy:toScreen(x, y, w, h) else f_neutral:toScreen(x, y, w, h) end end end end local e for i = 1, #ps do e = ps[i] e:checkDisplay() if e.ps:isAlive() then e.ps:toScreen(x + w / 2, y + h / 2, true, w / (game.level and game.level.map.tile_w or w)) else self:removeParticles(e) end end return true end) end function _M:move(x, y, force) local moved = false local ox, oy = self.x, self.y if force or self:enoughEnergy() then -- Confused ? if not force and self:attr("confused") then if rng.percent(self:attr("confused")) then x, y = self.x + rng.range(-1, 1), self.y + rng.range(-1, 1) end end -- Encased in ice, attack the ice if not force and self:attr("encased_in_ice") then self:attackTarget(self) moved = true -- Should we prob travel through walls ? elseif not force and self:attr("prob_travel") and game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move", self) then moved = self:probabilityTravel(x, y, self:attr("prob_travel")) -- Never move but tries to attack ? ok elseif not force and self:attr("never_move") then -- A bit weird, but this simple asks the collision code to detect an attack if not game.level.map:checkAllEntities(x, y, "block_move", self, true) then game.logPlayer(self, "You are unable to move!") end else moved = engine.Actor.move(self, x, y, force) end if not force and moved and (self.x ~= ox or self.y ~= oy) and not self.did_energy then self:useEnergy(game.energy_to_act * self:combatMovementSpeed()) end end self.did_energy = nil -- Try to detect traps if self:knowTalent(self.T_TRAP_DETECTION) then local power = self:getTalentLevel(self.T_TRAP_DETECTION) * self:getCun(25, true) local grids = core.fov.circle_grids(self.x, self.y, 1, true) for x, yy in pairs(grids) do for y, _ in pairs(yy) do local trap = game.level.map(x, y, Map.TRAP) if trap and not trap:knownBy(self) and self:checkHit(power, trap.detect_power) then trap:setKnown(self, true) game.level.map:updateMap(x, y) game.logPlayer(self, "You have found a trap (%s)!", trap:getName()) end end end end if moved and self:knowTalent(self.T_CURSED_TOUCH) then local t = self:getTalentFromId(self.T_CURSED_TOUCH) t.curseFloor(self, t, x, y) end if moved and self:isTalentActive(self.T_BODY_OF_STONE) then self:forceUseTalent(self.T_BODY_OF_STONE, {ignore_energy=true}) end if moved and not force and ox and oy and (ox ~= self.x or oy ~= self.y) and config.settings.tome.smooth_move > 0 then local blur = 0 if self:attr("lightning_speed") or self:attr("step_up") or self:attr("wild_speed") then blur = 3 end self:setMoveAnim(ox, oy, config.settings.tome.smooth_move, blur) end return moved end --- Knock back the actor -- Overloaded to add move anim function _M:knockback(srcx, srcy, dist, recursive) local ox, oy = self.x, self.y engine.Actor.knockback(self, srcx, srcy, dist, recursive) if config.settings.tome.smooth_move > 0 then self:resetMoveAnim() self:setMoveAnim(ox, oy, 9, 5) end end --- Pull in the actor -- Overloaded to add move anim function _M:pull(srcx, srcy, dist, recursive) local ox, oy = self.x, self.y engine.Actor.pull(self, srcx, srcy, dist, recursive) if config.settings.tome.smooth_move > 0 then self:resetMoveAnim() self:setMoveAnim(ox, oy, 9, 5) end end --- Get the "path string" for this actor -- See Map:addPathString() for more info function _M:getPathString() local ps = self.open_door and "return {open_door=true,can_pass={" or "return {can_pass={" for what, check in pairs(self.can_pass) do ps = ps .. what.."="..check.."," end ps = ps.."}}" -- print("[PATH STRING] for", self.name, " :=: ", ps) return ps end --- Drop no-teleport items function _M:dropNoTeleportObjects() for inven_id, inven in pairs(self.inven) do for item = #inven, 1, -1 do local o = inven[item] if o.no_teleport then self:dropFloor(inven, item, false, true) game.logPlayer(self, "#LIGHT_RED#Your %s is immune to the teleportation and drops to the floor!", o:getName{do_color=true}) end end end end --- Blink through walls function _M:probabilityTravel(x, y, dist) if game.zone.wilderness then return true end if self:attr("encased_in_ice") then return end local dirx, diry = x - self.x, y - self.y local tx, ty = x, y while game.level.map:isBound(tx, ty) and game.level.map:checkAllEntities(tx, ty, "block_move", self) and dist > 0 do if game.level.map.attrs(tx, ty, "no_teleport") then break end if game.level.map:checkAllEntities(tx, ty, "no_prob_travel", self) then break end tx = tx + dirx ty = ty + diry dist = dist - 1 end if game.level.map:isBound(tx, ty) and not game.level.map:checkAllEntities(tx, ty, "block_move", self) and not game.level.map.attrs(tx, ty, "no_teleport") then self:dropNoTeleportObjects() return engine.Actor.move(self, tx, ty, false) end return true end --- Teleports randomly to a passable grid -- This simply calls the default actor teleportRandom but first checks for space-time stability -- @param x the coord of the teleportation -- @param y the coord of the teleportation -- @param dist the radius of the random effect, if set to 0 it is a precise teleport -- @param min_dist the minimum radius of of the effect, will never teleport closer. Defaults to 0 if not set -- @return true if the teleport worked function _M:teleportRandom(x, y, dist, min_dist) if self:attr("encased_in_ice") then return end if game.level.data.no_teleport_south and y + dist > self.y then y = self.y - dist end local ox, oy = self.x, self.y local ret = engine.Actor.teleportRandom(self, x, y, dist, min_dist) if self.x ~= ox or self.y ~= oy then self.x, self.y, ox, oy = ox, oy, self.x, self.y self:dropNoTeleportObjects() self.x, self.y, ox, oy = ox, oy, self.x, self.y end return ret end --- Quake a zone -- Moves randomly each grid to an other grid function _M:doQuake(tg, x, y) local w = game.level.map.w local locs = {} local ms = {} self:project(tg, x, y, function(tx, ty) if not game.level.map.attrs(tx, ty, "no_teleport") then locs[#locs+1] = {x=tx,y=ty} ms[#ms+1] = {map=game.level.map.map[tx + ty * w], attrs=game.level.map.attrs[tx + ty * w]} end end) while #locs > 0 do local l = rng.tableRemove(locs) local m = rng.tableRemove(ms) game.level.map.map[l.x + l.y * w] = m.map game.level.map.attrs[l.x + l.y * w] = m.attrs for z, e in pairs(m.map or {}) do if e.move then e.x = nil e.y = nil e:move(l.x, l.y, true) end game.nicer_tiles:updateAround(game.level, l.x, l.y) end end game.level.map:cleanFOV() game.level.map.changed = true game.level.map:redisplay() end --- Reveals location surrounding the actor function _M:magicMap(radius, x, y, checker) x = x or self.x y = y or self.y radius = math.floor(radius) local ox, oy self.x, self.y, ox, oy = x, y, self.x, self.y self:computeFOV(radius, "block_sense", function(x, y) if not checker or checker(x, y) then game.level.map.remembers(x, y, true) game.level.map.has_seens(x, y, true) end end, true, true, true) self.x, self.y = ox, oy end --- What is our reaction toward the target -- This can modify faction reaction using specific actor to actor reactions function _M:reactionToward(target, no_reflection) if target == self and self:attr("encased_in_ice") then return -100 end local v = engine.Actor.reactionToward(self, target) if self.reaction_actor and self.reaction_actor[target.unique or target.name] then v = v + self.reaction_actor[target.unique or target.name] end -- Take the lowest of the two just in case if not no_reflection and target.reactionToward then v = math.min(v, target:reactionToward(self, true)) end return util.bound(v, -100, 100) end function _M:incMoney(v) self.money = self.money + v if self.money < 0 then self.money = 0 end self.changed = true if self.player then world:gainAchievement("TREASURE_HUNTER", self) world:gainAchievement("TREASURE_HOARDER", self) world:gainAchievement("DRAGON_GREED", self) end end function _M:getRankStatAdjust() if self.rank == 1 then return -1 elseif self.rank == 2 then return -0.5 elseif self.rank == 3 then return 0 elseif self.rank == 3.5 then return 1 elseif self.rank == 4 then return 1 elseif self.rank >= 5 then return 1 else return 0 end end function _M:getRankLevelAdjust() if self.rank == 1 then return -1 elseif self.rank == 2 then return 0 elseif self.rank == 3 then return 1 elseif self.rank == 3.5 then return 2 elseif self.rank == 4 then return 3 elseif self.rank >= 5 then return 4 else return 0 end end function _M:getRankVimAdjust() if self.rank == 1 then return 0.7 elseif self.rank == 2 then return 1 elseif self.rank == 3 then return 1.2 elseif self.rank == 3.5 then return 2.2 elseif self.rank == 4 then return 2.6 elseif self.rank >= 5 then return 2.8 else return 0 end end function _M:getRankLifeAdjust(value) local level_adjust = 1 + self.level / 40 if self.rank == 1 then return value * (level_adjust - 0.2) elseif self.rank == 2 then return value * (level_adjust - 0.1) elseif self.rank == 3 then return value * (level_adjust + 0.1) elseif self.rank == 3.5 then return value * (level_adjust + 1) elseif self.rank == 4 then return value * (level_adjust + 2) elseif self.rank >= 5 then return value * (level_adjust + 3) else return 0 end end function _M:getRankResistAdjust() if self.rank == 1 then return 0.4, 0.9 elseif self.rank == 2 then return 0.5, 1.5 elseif self.rank == 3 then return 0.8, 1.5 elseif self.rank == 3.5 then return 0.9, 1.5 elseif self.rank == 4 then return 0.9, 1.5 elseif self.rank >= 5 then return 0.9, 1.5 else return 0 end end function _M:TextRank() local rank, color = "normal", "#ANTIQUE_WHITE#" if self.rank == 1 then rank, color = "critter", "#C0C0C0#" elseif self.rank == 2 then rank, color = "normal", "#ANTIQUE_WHITE#" elseif self.rank == 3 then rank, color = "elite", "#YELLOW#" elseif self.rank == 3.5 then rank, color = "unique", "#SANDY_BROWN#" elseif self.rank == 4 then rank, color = "boss", "#ORANGE#" elseif self.rank >= 5 then rank, color = "elite boss", "#GOLD#" end return rank, color end function _M:TextSizeCategory() local sizecat = "medium" if self.size_category <= 1 then sizecat = "tiny" elseif self.size_category == 2 then sizecat = "small" elseif self.size_category == 3 then sizecat = "medium" elseif self.size_category == 4 then sizecat = "big" elseif self.size_category == 5 then sizecat = "huge" elseif self.size_category >= 6 then sizecat = "gargantuan" end return sizecat end function _M:tooltip(x, y, seen_by) if seen_by and not seen_by:canSee(self) then return end local factcolor, factstate, factlevel = "#ANTIQUE_WHITE#", "neutral", Faction:factionReaction(self.faction, game.player.faction) if factlevel < 0 then factcolor, factstate = "#LIGHT_RED#", "hostile" elseif factlevel > 0 then factcolor, factstate = "#LIGHT_GREEN#", "friendly" end -- Debug feature, mousing over with ctrl pressed will give detailed FOV info if config.settings.cheat and core.key.modState("ctrl") then print("============================================== SEEING from", self.name) for i, a in ipairs(self.fov.actors_dist) do local d = self.fov.actors[a] if d then print(("%3d : %-40s at %3dx%3d (see at %3dx%3d), diff %3dx%3d"):format(d.sqdist, a.name, a.x, a.y, d.x, d.y,d.dx,d.dy)) end end print("==============================================") end local pfactcolor, pfactstate, pfactlevel = "#ANTIQUE_WHITE#", "neutral", self:reactionToward(game.player) if pfactlevel < 0 then pfactcolor, pfactstate = "#LIGHT_RED#", "hostile" elseif pfactlevel > 0 then pfactcolor, pfactstate = "#LIGHT_GREEN#", "friendly" end local rank, rank_color = self:TextRank() local resists = {} for t, v in pairs(self.resists) do if t ~= "all" then v = self:combatGetResist(t) end resists[#resists+1] = string.format("%d%% %s", v, t == "all" and "all" or DamageType:get(t).name) end local ts = tstring{} ts:add({"uid",self.uid}) ts:merge(rank_color:toTString()) ts:add(self.name, {"color", "WHITE"}) if self.type == "humanoid" or self.type == "giant" then ts:add({"font","italic"}, "(", self.female and "female" or "male", ")", {"font","normal"}, true) else ts:add(true) end ts:add(self.type:capitalize(), " / ", self.subtype:capitalize(), true) ts:add("Rank: ") ts:merge(rank_color:toTString()) ts:add(rank, {"color", "WHITE"}, true) ts:add({"color", 0, 255, 255}, ("Level: %d"):format(self.level), {"color", "WHITE"}, true) ts:add(("Exp: %d/%d"):format(self.exp, self:getExpChart(self.level+1) or "---"), true) ts:add({"color", 255, 0, 0}, ("HP: %d (%d%%)"):format(self.life, self.life * 100 / self.max_life), {"color", "WHITE"}, true) if self:attr("encased_in_ice") then local eff = self:hasEffect(self.EFF_FROZEN) ts:add({"color", 0, 255, 128}, ("Iceblock: %d"):format(eff.hp), {"color", "WHITE"}, true) end ts:add(("Stats: %d / %d / %d / %d / %d / %d"):format(self:getStr(), self:getDex(), self:getCon(), self:getMag(), self:getWil(), self:getCun()), true) ts:add("Resists: ", table.concat(resists, ','), true) ts:add("Armour/Defense: ", tostring(math.floor(self:combatArmor())), ' / ', tostring(math.floor(self:combatDefense())), true) ts:add("Size: ", {"color", "ANTIQUE_WHITE"}, self:TextSizeCategory(), {"color", "WHITE"}, true) if self.summon_time then ts:add("Time left: ", {"color", "ANTIQUE_WHITE"}, ("%d"):format(self.summon_time), {"color", "WHITE"}, true) end ts:add(self.desc, true) if self.faction and Faction.factions[self.faction] then ts:add("Faction: ") ts:merge(factcolor:toTString()) ts:add(("%s (%s, %d)"):format(Faction.factions[self.faction].name, factstate, factlevel), {"color", "WHITE"}, true) end ts:add("Personal reaction: ") ts:merge(pfactcolor:toTString()) ts:add(("%s, %d"):format(pfactstate, pfactlevel), {"color", "WHITE"}, true) for tid, act in pairs(self.sustain_talents) do if act then ts:add("- ", {"color", "LIGHT_GREEN"}, self:getTalentFromId(tid).name, {"color", "WHITE"}, true) end end for eff_id, p in pairs(self.tmp) do local e = self.tempeffect_def[eff_id] local dur = p.dur + 1 if e.status == "detrimental" then if act then ts:add("- ", {"color", "LIGHT_RED"}, ("%s(%d)"):format(e.desc,dur), {"color", "WHITE"}, true) end else if act then ts:add("- ", {"color", "LIGHT_GREEN"}, ("%s(%d)"):format(e.desc,dur), {"color", "WHITE"}, true) end end end return ts end --- Regenerate life, call it from your actor class act() method function _M:regenLife() if self.life_regen and not self:attr("no_life_regen") then self.life = util.bound(self.life + self.life_regen * util.bound((self.healing_factor or 1), 0, 2.5), 0, self.max_life) end end --- Called before healing function _M:onHeal(value, src) if self:hasEffect(self.EFF_UNSTOPPABLE) then return 0 end if self:attr("encased_in_ice") then return 0 end value = value * util.bound((self.healing_factor or 1), 0, 2.5) if self:attr("stunned") then value = value / 2 end local eff = self:hasEffect(self.EFF_HEALING_NEXUS) if eff and value > 0 and not self.heal_leech_active then eff.src.heal_leech_active = true eff.src:heal(value * eff.pct, src) eff.src.heal_leech_active = nil eff.src:incEquilibrium(-eff.eq) if eff.src == self then game.logSeen(self, "%s heal is doubled!", self.name) else game.logSeen(self, "%s steals %s heal!", eff.src.name:capitalize(), self.name) return 0 end end if self:attr("arcane_shield") and value > 0 and not self:hasEffect(self.EFF_DAMAGE_SHIELD) then self:setEffect(self.EFF_DAMAGE_SHIELD, 3, {power=value * self.arcane_shield / 100}) end print("[HEALING]", self.uid, self.name, "for", value) return value end --- Called before taking a hit, it's the chance to check for shields function _M:onTakeHit(value, src) -- Un-daze if self:hasEffect(self.EFF_DAZED) then self:removeEffect(self.EFF_DAZED) end -- Un-meditate if self:hasEffect(self.EFF_MEDITATION) then self:removeEffect(self.EFF_MEDITATION) end if self:hasEffect(self.EFF_SPACETIME_TUNING) then self:removeEffect(self.EFF_SPACETIME_TUNING) end -- remove stalking if there is an interaction if self.stalker and src and self.stalker == src then self.stalker:removeEffect(self.EFF_STALKER) self:removeEffect(self.EFF_STALKED) end -- Remove domination hex if self:hasEffect(self.EFF_DOMINATION_HEX) and src and src == self:hasEffect(self.EFF_DOMINATION_HEX).src then self:removeEffect(self.EFF_DOMINATION_HEX) end if self:attr("invulnerable") then return 0 end if self:attr("retribution") then -- Absorb damage into the retribution if value / 2 <= self.retribution_absorb then self.retribution_absorb = self.retribution_absorb - (value / 2) value = value / 2 else value = value - self.retribution_absorb self.retribution_absorb = 0 local dam = self.retribution_strike -- Deactivate without loosing energy self:forceUseTalent(self.T_RETRIBUTION, {ignore_energy=true}) -- Explode! game.logSeen(self, "%s unleashes the stored damage in retribution!", self.name:capitalize()) local tg = {type="ball", range=0, radius=self:getTalentRange(self:getTalentFromId(self.T_RETRIBUTION)), selffire=false, talent=t} local grids = self:project(tg, self.x, self.y, DamageType.LIGHT, dam) game.level.map:particleEmitter(self.x, self.y, tg.radius, "sunburst", {radius=tg.radius, grids=grids, tx=self.x, ty=self.y}) end end if self:knowTalent(self.T_DISPLACE_DAMAGE) and self:isTalentActive(self.T_DISPLACE_DAMAGE) and rng.percent(5 + (self:getTalentLevel(self.T_DISPLACE_DAMAGE) * 5)) then -- find available targets local tgts = {} local grids = core.fov.circle_grids(self.x, self.y, self:getTalentLevelRaw(self.T_DISPLACE_DAMAGE) * 2, true) for x, yy in pairs(grids) do for y, _ in pairs(grids[x]) do local a = game.level.map(x, y, Map.ACTOR) if a and self:reactionToward(a) < 0 then tgts[#tgts+1] = a end end end -- Randomly take targets -- local tg = {type="hit"} for i = 1, 1 do if #tgts <= 0 then break end local a, id = rng.table(tgts) table.remove(tgts, id) if a then game.logSeen(self, "Some of the damage has been displaced onto %s!", a.name:capitalize()) a:takeHit(value / 2, src) value = value / 2 end end end if self:attr("disruption_shield") then local mana = self:getMana() local mana_val = value * self:attr("disruption_shield") -- We have enough to absorb the full hit if mana_val <= mana then self:incMana(-mana_val) self.disruption_shield_absorb = self.disruption_shield_absorb + value return 0 -- Or the shield collapses in a deadly arcane explosion else local dam = self.disruption_shield_absorb -- Deactivate without loosing energy self:forceUseTalent(self.T_DISRUPTION_SHIELD, {ignore_energy=true}) -- Explode! game.logSeen(self, "%s's disruption shield collapses and then explodes in a powerful manastorm!", self.name:capitalize()) local tg = {type="ball", radius=5} self:project(tg, self.x, self.y, DamageType.ARCANE, dam, {type="manathrust"}) end end if self:attr("time_shield") then -- Absorb damage into the time shield self.time_shield_absorb = self.time_shield_absorb or 0 if value <= self.time_shield_absorb then self.time_shield_absorb = self.time_shield_absorb - value value = 0 else value = value - self.time_shield_absorb self.time_shield_absorb = 0 end -- If we are at the end of the capacity, release the time shield damage if self.time_shield_absorb <= 0 then game.logPlayer(self, "Your time shield crumbles under the damage!") self:removeEffect(self.EFF_TIME_SHIELD) end end if self:attr("damage_shield") then -- Absorb damage into the shield self.damage_shield_absorb = self.damage_shield_absorb or 0 if value <= self.damage_shield_absorb then self.damage_shield_absorb = self.damage_shield_absorb - value value = 0 else value = value - self.damage_shield_absorb self.damage_shield_absorb = 0 end -- If we are at the end of the capacity, release the time shield damage if self.damage_shield_absorb <= 0 then game.logPlayer(self, "Your shield crumbles under the damage!") self:removeEffect(self.EFF_DAMAGE_SHIELD) end end if self:attr("displacement_shield") then -- Absorb damage into the displacement shield if rng.percent(self.displacement_shield_chance) then if value <= self.displacement_shield then game.logSeen(self, "The displacement shield teleports the damage to %s!", self.displacement_shield_target.name) self.displacement_shield = self.displacement_shield - value self.displacement_shield_target:takeHit(value, src) value = 0 else self:removeEffect(self.EFF_DISPLACEMENT_SHIELD) end end end if self:attr("repulsion_shield") then -- Absorb damage into the shield if value <= self.repulsion_shield_absorb then self.repulsion_shield_absorb = self.repulsion_shield_absorb - value value = 0 else value = value - self.repulsion_shield_absorb self.repulsion_shield_absorb = 0 end -- If we are at the end of the capacity, remove the effect if self.repulsion_shield_absorb <= 0 then game.logPlayer(self, "Your repulsion shield crumbles under the damage!") self:removeEffect(self.EFF_REPULSION_SHIELD) end end if self:attr("damage_shunt") then -- Absorb damage into the shield if value <= self.damage_shunt_absorb then self.damage_shunt_absorb = self.damage_shunt_absorb - value value = 0 else value = value - self.damage_shunt_absorb self.damage_shunt_absorb = 0 end -- If we are at the end of the capacity, remove the effect if self.damage_shunt_absorb <= 0 then game.logPlayer(self, "Your damage shunt spell has done all it can!") self:removeEffect(self.EFF_DAMAGE_SHUNT) end end if self:hasEffect(self.EFF_BONE_SHIELD) then local e = self.tempeffect_def[self.EFF_BONE_SHIELD] e.absorb(self, self.tmp[self.EFF_BONE_SHIELD]) value = 0 end if self:isTalentActive(self.T_BONE_SHIELD) then local t = self:getTalentFromId(self.T_BONE_SHIELD) t.absorb(self, t, self:isTalentActive(self.T_BONE_SHIELD)) value = 0 end if self:hasEffect(self.EFF_FORESIGHT) and value >= (self.max_life / 10) then self:removeEffect(self.EFF_FORESIGHT) game.logSeen(self, "%s avoids the attack.", self.name:capitalize()) value = 0 end if self:isTalentActive(self.T_DEFLECTION) then local t = self:getTalentFromId(self.T_DEFLECTION) value = t.do_onTakeHit(self, t, self:isTalentActive(self.T_DEFLECTION), value) end -- Mount takes some damage ? local mount = self:hasMount() if mount and mount.mount.share_damage then mount.mount.actor:takeHit(value * mount.mount.share_damage / 100, src) value = value * (100 - mount.mount.share_damage) / 100 -- Remove the dead mount if mount.mount.actor.dead and mount.mount.effect then self:removeEffect(mount.mount.effect) end end -- Achievements if not self.no_take_hit_achievements and src and src.resolveSource and src:resolveSource().player and value >= 600 then local rsrc = src:resolveSource() world:gainAchievement("SIZE_MATTERS", rsrc) if value >= 1500 then world:gainAchievement("DAMAGE_1500", rsrc) end if value >= 3000 then world:gainAchievement("DAMAGE_3000", rsrc) end if value >= 6000 then world:gainAchievement("DAMAGE_6000", rsrc) end end -- Stoned ? SHATTER ! if self:attr("stoned") and value >= self.max_life * 0.3 then -- Make the damage high enough to kill it value = self.max_life + 1 game.logSeen(self, "%s shatters into pieces!", self.name:capitalize()) end -- Frozen: absorb some damage into the iceblock if self:attr("encased_in_ice") then local eff = self:hasEffect(self.EFF_FROZEN) eff.hp = eff.hp - value * 0.4 value = value * 0.6 if eff.hp < 0 then self:removeEffect(self.EFF_FROZEN) end end -- Adds hate if self:knowTalent(self.T_HATE_POOL) then local hateGain = 0 local hateMessage if value / self.max_life >= 0.15 then -- you take a big hit..adds 0.2 + 0.2 for each 5% over 15% hateGain = hateGain + 0.2 + (((value / self.max_life) - 0.15) * 10 * 0.5) hateMessage = "#F53CBE#You fight through the pain!" end if value / self.max_life >= 0.05 and (self.life - value) / self.max_life < 0.25 then -- you take a hit with low health hateGain = hateGain + 0.4 hateMessage = "#F53CBE#Your rage grows even as your life fades!" end if hateGain >= 0.1 then self.hate = math.min(self.max_hate, self.hate + hateGain) if hateMessage then game.logPlayer(self, hateMessage.." (+%0.1f hate)", hateGain) end end end if src and (src.hate_per_powerful_hit or 0) > 0 and src.knowTalent and src:knowTalent(src.T_HATE_POOL) then local hateGain = 0 local hateMessage if value / src.max_life > 0.33 then -- you deliver a big hit hateGain = hateGain + src.hate_per_powerful_hit hateMessage = "#F53CBE#Your powerful attack feeds your madness!" end if hateGain >= 0.1 then src.hate = math.min(src.max_hate, src.hate + hateGain) if hateMessage then game.logPlayer(src, hateMessage.." (+%0.1f hate)", hateGain) end end end -- Bloodlust! if src and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then src:setEffect(src.EFF_BLOODLUST, 1, {}) end if self:knowTalent(self.T_RAMPAGE) then local t = self:getTalentFromId(self.T_RAMPAGE) t:onTakeHit(self, value / self.max_life) end if self:hasEffect(self.EFF_UNSTOPPABLE) then if value > self.life then value = self.life - 1 end end -- BEGIN CUSTOM GLOOM HATE REGEN EFFECTS -- if src and src.knowTalent and src:knowTalent(src.T_HATE_POOL) and (self:hasEffect(self.EFF_GLOOM_CONFUSED) or self:hasEffect(self.EFF_GLOOM_SLOW) or self:hasEffect(self.EFF_GLOOM_STUNNED) ) then local hateGain = 0.1 * (self.rank or 1) game.logPlayer(src, "#F53CBE#Your cruel attack feeds your madness! (+%0.2f hate)", hateGain) src.hate = math.min(src.max_hate, src.hate + hateGain) end -- END CUSTOM GLOOM HATE REGEN EFFECTS -- -- Split ? if self.clone_on_hit and value >= self.clone_on_hit.min_dam_pct * self.max_life / 100 and rng.percent(self.clone_on_hit.chance) then -- Find space local x, y = util.findFreeGrid(self.x, self.y, 1, true, {[Map.ACTOR]=true}) if x then -- Find a place around to clone local a = self:clone() a.life = math.max(1, a.life - value / 2) a.clone_on_hit.chance = math.ceil(a.clone_on_hit.chance / 2) a.energy.val = 0 a.exp_worth = 0.1 a.inven = {} a:removeAllMOs() a.x, a.y = nil, nil game.zone:addEntity(game.level, a, "actor", x, y) game.logSeen(self, "%s is split in two!", self.name:capitalize()) value = value / 2 end end if self.on_takehit then value = self:check("on_takehit", value, src) end -- Shield of Light if value > 0 and self:isTalentActive(self.T_SHIELD_OF_LIGHT) then if value <= 2 then drain = value else drain = 2 end if self:getPositive() <= 0 then self:forceUseTalent(self.T_SHIELD_OF_LIGHT, {ignore_energy=true}) game.logSeen(self, "%s's shield of light spell has crumbled under the attack!", self.name:capitalize()) else self:incPositive(- drain) self:heal(self:combatTalentSpellDamage(self.T_SHIELD_OF_LIGHT, 5, 25), self) end end -- Second Life if self:isTalentActive(self.T_SECOND_LIFE) and value >= self.life then local sl = self.max_life * (0.05 + self:getTalentLevelRaw(self.T_SECOND_LIFE)/25) value = 0 self.life = sl game.logSeen(self, "%s has been saved by a blast of positive energy!", self.name:capitalize()) self:forceUseTalent(self.T_SECOND_LIFE, {ignore_energy=true}) end -- Unflinching Resolve if self:knowTalent(self.T_UNFLINCHING_RESOLVE) and value >= (self.max_life / 10) then local t = self:getTalentFromId(self.T_UNFLINCHING_RESOLVE) local dam = value t.on_hit(self, t, dam) end -- Shade's reform if value >= self.life and self.ai_state and self.ai_state.can_reform then local t = self:getTalentFromId(self.T_SHADOW_REFORM) if rng.percent(t.getChance(self, t)) then value = 0 self.life = self.max_life game.logSeen(self, "%s fades for a moment and then reforms whole again!", self.name:capitalize()) game.level.map:particleEmitter(self.x, self.y, 1, "teleport_out") game:playSoundNear(self, "talents/heal") game.level.map:particleEmitter(self.x, self.y, 1, "teleport_in") end end -- Vim leech if self:knowTalent(self.T_LEECH) and src.hasEffect and src:hasEffect(src.EFF_VIMSENSE) then self:incVim(3 + self:getTalentLevel(self.T_LEECH) * 0.7) self:heal(5 + self:getTalentLevel(self.T_LEECH) * 3) game.logPlayer(self, "#AQUAMARINE#You leech a part of %s vim.", src.name:capitalize()) end -- Invisible on hit if value >= self.max_life * 0.15 and self:attr("invis_on_hit") and rng.percent(self:attr("invis_on_hit")) then self:setEffect(self.EFF_INVISIBILITY, 5, {power=self:attr("invis_on_hit_power")}) for tid, _ in pairs(self.invis_on_hit_disable) do self:forceUseTalent(tid, {ignore_energy=true}) end end -- Damage shield on hit if self:attr("contingency") and value >= self.max_life * self:attr("contingency") / 100 and not self:hasEffect(self.EFF_DAMAGE_SHIELD) then self:setEffect(self.EFF_DAMAGE_SHIELD, 3, {power=value * self:attr("contingency_shield") / 100}) for tid, _ in pairs(self.contingency_disable) do self:forceUseTalent(tid, {ignore_energy=true}) end end -- Life leech if value > 0 and src and src:attr("life_leech_chance") and rng.percent(src.life_leech_chance) then local leech = math.min(value, self.life) * src.life_leech_value / 100 src:heal(leech) game.logSeen(src, "#CRIMSON#%s leeches life from its victim!", src.name:capitalize()) end -- Resource leech if value > 0 and src and src:attr("resource_leech_chance") and rng.percent(src.resource_leech_chance) then local leech = src.resource_leech_value src:incMana(leech) src:incVim(leech * 0.5) src:incPositive(leech * 0.25) src:incNegative(leech * 0.25) src:incEquilibrium(-leech * 0.35) src:incStamina(leech * 0.65) src:incHate(leech * 0.05) src:incPsi(leech * 0.2) game.logSeen(src, "#CRIMSON#%s leeches energies from its victim!", src.name:capitalize()) end return value end function _M:removeTimedEffectsOnClone() local todel = {} for eff, p in pairs(self.tmp) do if _M.tempeffect_def[eff].remove_on_clone then todel[#todel+1] = eff end end while #todel > 0 do self:removeEffect(table.remove(todel)) end end function _M:resolveSource() if self.summoner_gain_exp and self.summoner then return self.summoner:resolveSource() else return self end end function _M:die(src) if self.dead then self:disappear(src) self:deleteFromMap(game.level.map) return true end engine.interface.ActorLife.die(self, src) -- Gives the killer some exp for the kill local killer = nil if src and src.resolveSource and src:resolveSource().gainExp then killer = src:resolveSource() killer:gainExp(self:worthExp(killer)) end -- Hack: even if the boss dies from something else, give the player exp if (not killer or not killer.player) and self.rank > 3 and not game.party:hasMember(self) then game.logPlayer(game.player, "You feel a surge of power as a powerful creature falls nearby.") killer = game.player:resolveSource() killer:gainExp(self:worthExp(killer)) end -- Register bosses deaths if self.rank > 3 then game.state:bossKilled(self.rank) end if self.on_death_lore then game.player:learnLore(self.on_death_lore) end -- Do we get a blooooooody death ? if rng.percent(33) then self:bloodyDeath() end -- Drop stuff if not self.keep_inven_on_death then if not self.no_drops then local invens = {} for inven_id, inven in pairs(self.inven) do invens[#invens+1] = inven end table.sort(invens, function(a,b) if a.id == 1 then return false elseif b.id == 1 then return true else return a.id < b.id end end) for _, inven in ipairs(invens) do for i, o in ipairs(inven) do -- Handle boss wielding artifacts if o.__special_boss_drop and rng.percent(o.__special_boss_drop.chance) then print("Refusing to drop "..self.name.." artifact "..o.name.." with chance "..o.__special_boss_drop.chance) -- Do not drop o.no_drop = true -- Drop a random artifact instead local ro = game.zone:makeEntity(game.level, "object", {no_tome_drops=true, unique=true, not_properties={"lore"}}, nil, true) if ro then game.zone:addEntity(game.level, ro, "object", self.x, self.y) end end if not o.no_drop then o.droppedBy = self.name game.level.map:addObject(self.x, self.y, o) else o:removed() end end end end self.inven = {} end -- Give stamina back if src and src.knowTalent and src:knowTalent(src.T_UNENDING_FRENZY) then src:incStamina(src:getTalentLevel(src.T_UNENDING_FRENZY) * 2) end -- Increases blood frenzy if src and src.knowTalent and src:knowTalent(src.T_BLOOD_FRENZY) and src:isTalentActive(src.T_BLOOD_FRENZY) then src.blood_frenzy = src.blood_frenzy + src:getTalentLevel(src.T_BLOOD_FRENZY) * 2 end -- Increases necrotic aura count if src and src.resolveSource and src:resolveSource().isTalentActive and src:resolveSource():isTalentActive(src.T_NECROTIC_AURA) then local rsrc = src:resolveSource() local p = rsrc:isTalentActive(src.T_NECROTIC_AURA) if self.x and self.y and src.x and src.y and core.fov.distance(self.x, self.y, rsrc.x, rsrc.y) <= rsrc.necrotic_aura_radius then p.souls = math.min(p.souls + 1, p.souls_max) rsrc.changed = true end end -- Adds hate if src and src.knowTalent and src:knowTalent(src.T_HATE_POOL) then local hateGain = src.hate_per_kill local hateMessage if self.level - 2 > src.level then -- level bonus hateGain = hateGain + (self.level - 2 - src.level) * 0.2 hateMessage = "#F53CBE#You have taken the life of an experienced foe!" end if self.rank >= 4 then -- boss bonus hateGain = hateGain * 4 hateMessage = "#F53CBE#Your hate has conquered a great adversary!" elseif self.rank >= 3 then -- elite bonus hateGain = hateGain * 2 hateMessage = "#F53CBE#An elite foe has fallen to your hate!" end hateGain = math.min(hateGain, 10) src.hate = math.min(src.max_hate, src.hate + hateGain) if hateMessage then game.logPlayer(src, hateMessage.." (+%0.1f hate)", hateGain - src.hate_per_kill) end end if src and src.summoner and src.summoner_hate_per_kill then if src.summoner.knowTalent and src.summoner:knowTalent(src.summoner.T_HATE_POOL) then src.summoner.hate = math.min(src.summoner.max_hate, src.summoner.hate + src.summoner_hate_per_kill) game.logPlayer(src.summoner, "%s feeds you hate from it's latest victim. (+%0.1f hate)", src.name:capitalize(), src.summoner_hate_per_kill) end end if src and src.knowTalent and src:knowTalent(src.T_UNNATURAL_BODY) then local t = src:getTalentFromId(src.T_UNNATURAL_BODY) t.on_kill(src, t, self) end if src and src.knowTalent and src:knowTalent(src.T_CRUEL_VIGOR) then local t = src:getTalentFromId(src.T_CRUEL_VIGOR) t.on_kill(src, t) end if src and src.knowTalent and src:knowTalent(src.T_BLOODRAGE) then local t = src:getTalentFromId(src.T_BLOODRAGE) t.on_kill(src, t) end if src and src.isTalentActive and src:isTalentActive(src.T_FORAGE) then local t = src:getTalentFromId(src.T_FORAGE) t.on_kill(src, t, self) end if src and src.knowTalent and src:knowTalent(src.T_TOXIC_DEATH) then local t = src:getTalentFromId(src.T_TOXIC_DEATH) t.on_kill(src, t, self) end if src and src.hasEffect and src:hasEffect(self.EFF_UNSTOPPABLE) then local p = src:hasEffect(self.EFF_UNSTOPPABLE) p.kills = p.kills + 1 end if src and src.knowTalent and src:knowTalent(src.T_STEP_UP) and rng.percent(src:getTalentLevelRaw(src.T_STEP_UP) * 20) then game:onTickEnd(function() src:setEffect(self.EFF_STEP_UP, 1, {}) end) end if self:hasEffect(self.EFF_CORROSIVE_WORM) then local p = self:hasEffect(self.EFF_CORROSIVE_WORM) p.src:project({type="ball", radius=4, x=self.x, y=self.y}, self.x, self.y, DamageType.ACID, p.explosion, {type="acid"}) end -- Increase vim if src and src.knowTalent and src:knowTalent(src.T_VIM_POOL) then src:incVim(1 + src:getWil() / 10) end if src and src.attr and src:attr("vim_on_death") and not self:attr("undead") then src:incVim(src:attr("vim_on_death")) end if src and src.last_vim_turn and src.last_vim_turn >= game.turn - 30 then src:incVim(src.last_vim_spent) src.last_vim_turn = nil end if src and ((src.resolveSource and src:resolveSource().player) or src.player) then -- Achievements local p = game.party:findMember{main=true} if math.floor(p.life) <= 1 and not p.dead then world:gainAchievement("THAT_WAS_CLOSE", p) end world:gainAchievement("BOSS_REVENGE", p, self) world:gainAchievement("EMANCIPATION", p, self) world:gainAchievement("EXTERMINATOR", p, self) world:gainAchievement("PEST_CONTROL", p, self) world:gainAchievement("REAVER", p, self) world:gainAchievement("EAT_BOSSES", p, self) if self.unique then game.player:registerUniqueKilled(self) end -- Record kills p.all_kills = p.all_kills or {} p.all_kills[self.name] = p.all_kills[self.name] or 0 p.all_kills[self.name] = p.all_kills[self.name] + 1 end return true end function _M:learnStats(statorder) self.auto_stat_cnt = self.auto_stat_cnt or 1 local nb = 0 local max = 60 -- Allow to go over a natural 60, up to 80 at level 50 if not self.no_auto_high_stats then max = 60 + (self.level * 20 / 50) end while self.unused_stats > 0 do if self:getStat(statorder[self.auto_stat_cnt]) < max then self:incIncStat(statorder[self.auto_stat_cnt], 1) self.unused_stats = self.unused_stats - 1 end self.auto_stat_cnt = util.boundWrap(self.auto_stat_cnt + 1, 1, #statorder) nb = nb + 1 if nb >= #statorder then break end end end function _M:resetToFull() if self.dead then return end self.life = self.max_life self.mana = self.max_mana self.vim = self.max_vim self.stamina = self.max_stamina self.equilibrium = self.min_equilibrium self.air = self.max_air self.psi = self.max_psi end function _M:levelup() engine.interface.ActorLevel.levelup(self) engine.interface.ActorTalents.resolveLevelTalents(self) if not self.no_points_on_levelup then self.unused_stats = self.unused_stats + (self.stats_per_level or 3) + self:getRankStatAdjust() self.unused_talents = self.unused_talents + 1 self.unused_generics = self.unused_generics + 1 if self.level % 5 == 0 then self.unused_talents = self.unused_talents + 1 end if self.level % 5 == 0 then self.unused_generics = self.unused_generics - 1 end -- At levels 10, 20 and 30 we gain a new talent type if self.level == 10 or self.level == 20 or self.level == 30 then self.unused_talents_types = self.unused_talents_types + 1 end elseif type(self.no_points_on_levelup) == "function" then self:no_points_on_levelup() end -- Gain some basic resistances if not self.no_auto_resists then -- Make up a random list of resists the first time if not self.auto_resists_list then local list = { DamageType.PHYSICAL, DamageType.FIRE, DamageType.COLD, DamageType.ACID, DamageType.LIGHTNING, DamageType.LIGHT, DamageType.DARKNESS, DamageType.NATURE, DamageType.BLIGHT, DamageType.TEMPORAL, DamageType.MIND, } self.auto_resists_list = {} for i = 1, rng.range(1, self.auto_resists_nb or 2) do local t = rng.tableRemove(list) -- Double the chance so that resist is more likely to happen if rng.percent(30) then self.auto_resists_list[#self.auto_resists_list+1] = t end self.auto_resists_list[#self.auto_resists_list+1] = t end end -- Provide one of our resists local t = rng.table(self.auto_resists_list) if (self.resists[t] or 0) < 50 then self.resists[t] = (self.resists[t] or 0) + rng.float(self:getRankResistAdjust()) end -- Bosses have a right to get a general damage reduction if self.rank >= 4 then self.resists.all = (self.resists.all or 0) + rng.float(self:getRankResistAdjust()) / (self.rank == 4 and 3 or 2.5) end end -- Gain life and resources local rating = self.life_rating if not self.fixed_rating then rating = rng.range(math.floor(self.life_rating * 0.5), math.floor(self.life_rating * 1.5)) end self.max_life = self.max_life + math.max(self:getRankLifeAdjust(rating), 1) self:incMaxVim(self.vim_rating) self:incMaxMana(self.mana_rating) self:incMaxStamina(self.stamina_rating) self:incMaxPositive(self.positive_negative_rating) self:incMaxNegative(self.positive_negative_rating) self:incMaxPsi(self.psi_rating) if self.max_hate < self.absolute_max_hate then local amount = math.min(self.hate_rating, self.absolute_max_hate - self.max_hate) self:incMaxHate(amount) end -- Heal up on new level self:resetToFull() -- Auto levelup ? if self.autolevel then engine.Autolevel:autoLevel(self) end -- Force levelup of the golem if self.alchemy_golem then self.alchemy_golem:forceLevelup(self.level) end -- Notify party levelups if self.x and self.y and game.party:hasMember(self) and not self.silent_levelup then local x, y = game.level.map:getTileToScreen(self.x, self.y) game.flyers:add(x, y, 80, 0.5, -2, "LEVEL UP!", {0,255,255}) game.log("#00ffff#Welcome to level %d [%s].", self.level, self.name:capitalize()) local more = "Press G to use them." if game.player ~= self then more = "Select "..self.name.. " in the party list and press G to use them." end local points = {} if self.unused_stats > 0 then points[#points+1] = ("%d stat point(s)"):format(self.unused_stats) end if self.unused_talents > 0 then points[#points+1] = ("%d class talent point(s)"):format(self.unused_talents) end if self.unused_generics > 0 then points[#points+1] = ("%d generic talent point(s)"):format(self.unused_generics) end if self.unused_talents_types > 0 then points[#points+1] = ("%d category point(s)"):format(self.unused_talents_types) end if #points > 0 then game.log("%s has %s to spend. %s", self.name:capitalize(), table.concat(points, ", "), more) end if self.level == 10 then world:gainAchievement("LEVEL_10", self) end if self.level == 20 then world:gainAchievement("LEVEL_20", self) end if self.level == 30 then world:gainAchievement("LEVEL_30", self) end if self.level == 40 then world:gainAchievement("LEVEL_40", self) end if self.level == 50 then world:gainAchievement("LEVEL_50", self) end if game.permadeath == game.PERMADEATH_MANY and ( self.level == 2 or self.level == 5 or self.level == 7 or self.level == 14 or self.level == 24 or self.level == 35 ) then self.easy_mode_lifes = (self.easy_mode_lifes or 0) + 1 game.logPlayer(self, "#AQUAMARINE#You have gained one more life (%d remaining).", self.easy_mode_lifes) end game:updateCurrentChar() end end --- Notifies a change of stat value function _M:onStatChange(stat, v) if stat == self.STAT_CON then self.max_life = self.max_life + 4 * v self:updateConDamageReduction() elseif stat == self.STAT_WIL then self:incMaxMana(5 * v) self:incMaxStamina(2.5 * v) self:incMaxPsi(1 * v) elseif stat == self.STAT_STR then self:checkEncumbrance() end end function _M:updateConDamageReduction() self.resists = self.resists or {} self.resists.all = self.resists.all or 0 if self.temp_con_perc then self.resists.all = self.resists.all - self.temp_con_perc end local inc = self:getCon() / 7 if self:knowTalent(self.T_IRON_SKIN) then inc = inc * (1 + (self:getTalentLevel(self.T_IRON_SKIN) * 0.2)) end self.temp_con_perc = inc self.resists.all = self.resists.all + inc end --- Called when a temporary value changes (added or deleted) -- Takes care to call onStatChange when needed -- @param prop the property changing -- @param v the value of the change -- @param base the base table of prop function _M:onTemporaryValueChange(prop, v, base) if base == self.inc_stats then self:onStatChange(prop, v) end end function _M:attack(target) self:bumpInto(target) end function _M:getMaxEncumbrance() local add = 0 return math.floor(40 + self:getStr() * 1.8 + (self.max_encumber or 0) + add) end function _M:getEncumbrance() local enc = 0 local fct = function(so) enc = enc + so.encumber end -- Compute encumbrance for inven_id, inven in pairs(self.inven) do for item, o in ipairs(inven) do o:forAllStack(fct) end end -- print("Total encumbrance", enc) return math.floor(enc) end function _M:checkEncumbrance() -- Compute encumbrance local enc, max = self:getEncumbrance(), self:getMaxEncumbrance() -- We are pinned to the ground if we carry too much if not self.encumbered and enc > max then game.logPlayer(self, "#FF0000#You carry too much--you are encumbered!") game.logPlayer(self, "#FF0000#Drop some of your items.") self.encumbered = self:addTemporaryValue("never_move", 1) elseif self.encumbered and enc <= max then self:removeTemporaryValue("never_move", self.encumbered) self.encumbered = nil game.logPlayer(self, "#00FF00#You are no longer encumbered.") end end --- Update tile for races that can handle it function _M:updateModdableTile() if not self.moddable_tile or Map.tiles.no_moddable_tiles then return end self:removeAllMOs() local base = "player/"..self.moddable_tile:gsub("#sex#", self.female and "female" or "male").."/" self.image = base.."base_shadow_01.png" self.add_mos = {} local add = self.add_mos local i i = self.inven[self.INVEN_CLOAK]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile):format("behind")..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end add[#add+1] = {image = base..(self.moddable_tile_base or "base_01.png")} i = self.inven[self.INVEN_CLOAK]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile):format("shoulder")..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end i = self.inven[self.INVEN_BODY]; if i and i[1] and i[1].moddable_tile2 then add[#add+1] = {image = base..(i[1].moddable_tile2)..".png"} elseif not self.moddable_tile_nude then add[#add+1] = {image = base.."lower_body_01.png"} end i = self.inven[self.INVEN_BODY]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile)..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} elseif not self.moddable_tile_nude then add[#add+1] = {image = base.."upper_body_01.png"} end i = self.inven[self.INVEN_MAINHAND]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile):format("right")..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} if i[1].moddable_tile_particle then add[#add].particle = i[1].moddable_tile_particle[1] add[#add].particle_args = i[1].moddable_tile_particle[2] end end i = self.inven[self.INVEN_OFFHAND]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile):format("left")..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end i = self.inven[self.INVEN_HEAD]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile)..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end i = self.inven[self.INVEN_FEET]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile)..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end i = self.inven[self.INVEN_HANDS]; if i and i[1] and i[1].moddable_tile then add[#add+1] = {image = base..(i[1].moddable_tile)..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end i = self.inven[self.INVEN_CLOAK]; if i and i[1] and i[1].moddable_tile_hood then add[#add+1] = {image = base..(i[1].moddable_tile):format("hood")..".png", display_y=i[1].moddable_tile_big and -1 or 0, display_h=i[1].moddable_tile_big and 2 or 1} end if self.moddable_tile_ornament and self.moddable_tile_ornament[self.female and "female" or "male"] then add[#add+1] = {image = base..self.moddable_tile_ornament[self.female and "female" or "male"]..".png"} end if self.x and game.level then game.level.map:updateMap(self.x, self.y) end end --- Call when an object is worn function _M:onWear(o) engine.interface.ActorInventory.onWear(self, o) if o.talent_on_spell then self.talent_on_spell = self.talent_on_spell or {} for i = 1, #o.talent_on_spell do local id = util.uuid() self.talent_on_spell[id] = o.talent_on_spell[i] o.talent_on_spell[i]._id = id end end self:updateModdableTile() end --- Call when an object is taken off function _M:onTakeoff(o) engine.interface.ActorInventory.onTakeoff(self, o) if o.talent_on_spell then self.talent_on_spell = self.talent_on_spell or {} for i = 1, #o.talent_on_spell do local id = o.talent_on_spell[i]._id self.talent_on_spell[id] = nil end end self:updateModdableTile() end --- Call when an object is added function _M:onAddObject(o) -- curse the item if self:knowTalent(self.T_CURSED_TOUCH) and not o.cursed_touch then local t = self:getTalentFromId(self.T_CURSED_TOUCH) t.curseItem(self, t, o) end engine.interface.ActorInventory.onAddObject(self, o) self:checkEncumbrance() -- Achievement checks if self.player then if o.unique and not o.lore and not o.randart then game.player:registerArtifactsPicked(o) end world:gainAchievement("DEUS_EX_MACHINA", self, o) end end --- Call when an object is removed function _M:onRemoveObject(o) engine.interface.ActorInventory.onRemoveObject(self, o) self:checkEncumbrance() end --- Returns the possible offslot function _M:getObjectOffslot(o) if o.dual_wieldable and self:attr("allow_any_dual_weapons") then return "OFFHAND" else return o.offslot end 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" end if o.power_source and o.power_source.antimagic and not self:attr("forbid_arcane") then return nil, "requires antimagic" end return engine.interface.ActorInventory.canWearObject(self, o, try_slot) end --- Actor learns a talent -- @param t_id the id of the talent to learn -- @return true if the talent was learnt, nil and an error message otherwise function _M:learnTalent(t_id, force, nb) if not engine.interface.ActorTalents.learnTalent(self, t_id, force, nb) then return false end -- If we learned a spell, get mana, if you learned a technique get stamina, if we learned a wild gift, get power local t = _M.talents_def[t_id] if t.dont_provide_pool then return true end if t.type[1]:find("^spell/") and not self:knowTalent(self.T_MANA_POOL) and t.mana or t.sustain_mana then self:learnTalent(self.T_MANA_POOL, true) self.resource_pool_refs[self.T_MANA_POOL] = (self.resource_pool_refs[self.T_MANA_POOL] or 0) + 1 end if t.type[1]:find("^wild%-gift/") and not self:knowTalent(self.T_EQUILIBRIUM_POOL) and t.equilibrium or t.sustain_equilibrium then self:learnTalent(self.T_EQUILIBRIUM_POOL, true) self.resource_pool_refs[self.T_EQUILIBRIUM_POOL] = (self.resource_pool_refs[self.T_EQUILIBRIUM_POOL] or 0) + 1 end if t.type[1]:find("^technique/") and not self:knowTalent(self.T_STAMINA_POOL) and t.stamina or t.sustain_stamina then self:learnTalent(self.T_STAMINA_POOL, true) self.resource_pool_refs[self.T_STAMINA_POOL] = (self.resource_pool_refs[self.T_STAMINA_POOL] or 0) + 1 end if t.type[1]:find("^corruption/") and not self:knowTalent(self.T_VIM_POOL) and t.vim or t.sustain_vim then self:learnTalent(self.T_VIM_POOL, true) self.resource_pool_refs[self.T_VIM_POOL] = (self.resource_pool_refs[self.T_VIM_POOL] or 0) + 1 end if t.type[1]:find("^celestial/") and (t.positive or t.sustain_positive) and not self:knowTalent(self.T_POSITIVE_POOL) then self:learnTalent(self.T_POSITIVE_POOL, true) self.resource_pool_refs[self.T_POSITIVE_POOL] = (self.resource_pool_refs[self.T_POSITIVE_POOL] or 0) + 1 end if t.type[1]:find("^celestial/") and (t.negative or t.sustain_negative) and not self:knowTalent(self.T_NEGATIVE_POOL) then self:learnTalent(self.T_NEGATIVE_POOL, true) self.resource_pool_refs[self.T_NEGATIVE_POOL] = (self.resource_pool_refs[self.T_NEGATIVE_POOL] or 0) + 1 end if t.type[1]:find("^cursed/") and not self:knowTalent(self.T_HATE_POOL) then self:learnTalent(self.T_HATE_POOL, true) self.resource_pool_refs[self.T_HATE_POOL] = (self.resource_pool_refs[self.T_HATE_POOL] or 0) + 1 end if t.type[1]:find("^chronomancy/") and not self:knowTalent(self.T_PARADOX_POOL) then self:learnTalent(self.T_PARADOX_POOL, true) self.resource_pool_refs[self.T_PARADOX_POOL] = (self.resource_pool_refs[self.T_PARADOX_POOL] or 0) + 1 end if t.type[1]:find("^psionic/") and not self:knowTalent(self.T_PSI_POOL) then self:learnTalent(self.T_PSI_POOL, true) self.resource_pool_refs[self.T_PSI_POOL] = (self.resource_pool_refs[self.T_PSI_POOL] or 0) + 1 end -- If we learn an archery talent, also learn to shoot if t.type[1]:find("^technique/archery") and not self:knowTalent(self.T_SHOOT) then self:learnTalent(self.T_SHOOT, true) self.resource_pool_refs[self.T_SHOOT] = (self.resource_pool_refs[self.T_SHOOT] or 0) + 1 end return true end --- Actor forgets a talent -- @param t_id the id of the talent to learn -- @return true if the talent was unlearnt, nil and an error message otherwise function _M:unlearnTalent(t_id) if not engine.interface.ActorTalents.unlearnTalent(self, t_id, force) then return false end -- Check the various pools for key, num_refs in pairs(self.resource_pool_refs) do if num_refs == 0 then self:unlearnTalent(key) end end return true end --- Equilibrium check function _M:equilibriumChance(eq) eq = (eq or 0) + self:getEquilibrium() local wil = self:getWil() -- Do not fail if below willpower if eq < wil then return true, 100 end eq = eq - wil local chance = math.sqrt(eq) / 60 --print("[Equilibrium] Use chance: ", 100 - chance * 100, "::", self:getEquilibrium()) chance = util.bound(chance, 0, 1) return rng.percent(100 - chance * 100), 100 - chance * 100 end --- Paradox checks function _M:paradoxChanceModifier() local modifier = self:getWil() if self:knowTalent(self.T_PARADOX_MASTERY) and self:isTalentActive(self.T_PARADOX_MASTERY) then modifier = self:getWil() * (1 + (self:getTalentLevel(self.T_PARADOX_MASTERY)/10) or 0 ) end --print("[Paradox] Will modifier: ", modifier, "::", self:getParadox()) return modifier end function _M:paradoxFailChance(pa) local chance = math.pow(((self:getParadox() - self:paradoxChanceModifier())/200), 2)*((100 + self:combatFatigue()) / 100) if self:getParadox() < 200 then chance = 0 end --print("[Paradox] Fail chance: ", chance, "::", self:getParadox()) chance = util.bound(chance, 0, 100) return rng.percent(chance), chance end function _M:paradoxAnomalyChance(pa) local chance = math.pow(((self:getParadox() - self:paradoxChanceModifier())/300), 3)*((100 + self:combatFatigue()) / 100) if self:getParadox() < 300 then chance = 0 end --print("[Paradox] Anomaly chance: ", chance, "::", self:getParadox()) chance = util.bound(chance, 0, 100) return rng.percent(chance), chance end function _M:paradoxBackfireChance(pa) local chance = math.pow (((self:getParadox() - self:paradoxChanceModifier())/400), 4)*((100 + self:combatFatigue()) / 100) if self:getParadox() < 400 then chance = 0 end --print("[Paradox] Backfire chance: ", chance, "::", self:getParadox()) chance = util.bound(chance, 0, 100) return rng.percent(chance), chance end -- Overwrite incParadox to set up threshold log messages local previous_incParadox = _M.incParadox function _M:incParadox(paradox) -- Failure checks if self:getParadox() < 200 and self:getParadox() + paradox >= 200 then game.logPlayer(self, "#LIGHT_RED#You feel the edges of time begin to fray!") end if self:getParadox() > 200 and self:getParadox() + paradox <= 200 then game.logPlayer(self, "#LIGHT_BLUE#Time feels more stable.") end -- Anomaly checks if self:getParadox() < 300 and self:getParadox() + paradox >= 300 then game.logPlayer(self, "#LIGHT_RED#You feel the edges of space begin to ripple and bend!") end if self:getParadox() > 300 and self:getParadox() + paradox <= 300 then game.logPlayer(self, "#LIGHT_BLUE#Space feels more stable.") end -- Backfire checks if self:getParadox() < 400 and self:getParadox() + paradox >= 400 then game.logPlayer(self, "#LIGHT_RED#Space and time both fight against your control!") end if self:getParadox() > 400 and self:getParadox() + paradox <= 400 then game.logPlayer(self, "#LIGHT_BLUE#Space and time have calmed... somewhat.") end return previous_incParadox(self, paradox) end --- Called before a talent is used -- Check the actor can cast it -- @param ab the talent (not the id, the table) -- @return true to continue, false to stop function _M:preUseTalent(ab, silent, fake) if self:attr("feared") and (ab.mode ~= "sustained" or not self:isTalentActive(ab.id)) then if not silent then game.logSeen(self, "%s is too afraid to use %s.", self.name:capitalize(), ab.name) end return false end -- When silenced you can deactivate spells but not activate them if ab.no_silence and self:attr("silence") and (ab.mode ~= "sustained" or not self:isTalentActive(ab.id)) then if not silent then game.logSeen(self, "%s is silenced and cannot use %s.", self.name:capitalize(), ab.name) end return false end if ab.is_spell and self:attr("forbid_arcane") and (ab.mode ~= "sustained" or not self:isTalentActive(ab.id)) then if not silent then game.logSeen(self, "The spell fizzles.") end return false end -- when using unarmed techniques check for weapons and heavy armor if ab.is_unarmed then -- first check for heavy and massive armor if self:hasMassiveArmor() then if not silent then game.logSeen(self, "You are to heavily armored to use this talent.") end return false -- next make sure we're unarmed elseif not self:isUnarmed() then if not silent then game.logSeen(self, "You can't use this talent while holding a weapon or shield.") end return false end end if not self:enoughEnergy() and not fake then return false end if self:attr("force_talent_ignore_ressources") then -- nothing elseif ab.mode == "sustained" then if ab.sustain_mana and self.max_mana < ab.sustain_mana and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough mana to activate %s.", ab.name) end return false end if ab.sustain_stamina and self.max_stamina < ab.sustain_stamina and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough stamina to activate %s.", ab.name) end return false end if ab.sustain_vim and self.max_vim < ab.sustain_vim and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough vim to activate %s.", ab.name) end return false end if ab.sustain_positive and self.max_positive < ab.sustain_positive and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough positive energy to activate %s.", ab.name) end return false end if ab.sustain_negative and self.max_negative < ab.sustain_negative and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough negative energy to activate %s.", ab.name) end return false end if ab.sustain_hate and self.max_hate < ab.sustain_hate and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough hate to activate %s.", ab.name) end return false end if ab.sustain_psi and self.max_psi < ab.sustain_psi and not self:isTalentActive(ab.id) then if not silent then game.logPlayer(self, "You do not have enough energy to activate %s.", ab.name) end return false end else if ab.mana and self:getMana() < util.getval(ab.mana, self, ab) * (100 + 2 * self:combatFatigue()) / 100 then if not silent then game.logPlayer(self, "You do not have enough mana to cast %s.", ab.name) end return false end if ab.stamina and self:getStamina() < ab.stamina * (100 + self:combatFatigue()) / 100 then if not silent then game.logPlayer(self, "You do not have enough stamina to use %s.", ab.name) end return false end if ab.vim and self:getVim() < ab.vim then if not silent then game.logPlayer(self, "You do not have enough vim to use %s.", ab.name) end return false end if ab.positive and self:getPositive() < ab.positive * (100 + self:combatFatigue()) / 100 then if not silent then game.logPlayer(self, "You do not have enough positive energy to use %s.", ab.name) end return false end if ab.negative and self:getNegative() < ab.negative * (100 + self:combatFatigue()) / 100 then if not silent then game.logPlayer(self, "You do not have enough negative energy to use %s.", ab.name) end return false end if ab.hate and self:getHate() < ab.hate then if not silent then game.logPlayer(self, "You do not have enough hate to use %s.", ab.name) end return false end if ab.psi and self:getPsi() < ab.psi * (100 + 2 * self:combatFatigue()) / 100 then if not silent then game.logPlayer(self, "You do not have enough energy to cast %s.", ab.name) end return false end end -- Equilibrium is special, it has no max, but the higher it is the higher the chance of failure (and loss of the turn) -- But it is not affected by fatigue if (ab.equilibrium or (ab.sustain_equilibrium and not self:isTalentActive(ab.id))) and not fake and not self:attr("force_talent_ignore_ressources") then -- Fail ? lose energy and 1/10 more equilibrium if not self:attr("no_equilibrium_fail") and not self:equilibriumChance(ab.equilibrium or ab.sustain_equilibrium) then if not silent then game.logPlayer(self, "You fail to use %s due to your equilibrium!", ab.name) end self:incEquilibrium((ab.equilibrium or ab.sustain_equilibrium) / 10) self:useEnergy() return false end end -- Paradox is special, it has no max, but the higher it is the higher the chance of something bad happening if (ab.paradox or (ab.sustain_paradox and not self:isTalentActive(ab.id))) and not fake and not self:attr("force_talent_ignore_ressources") then -- Check failure first if not self:attr("no_paradox_fail") and self:paradoxFailChance(ab.paradox or ab.sustain_paradox) then if not silent then game.logPlayer(self, "You fail to use %s due to your paradox!", ab.name) end self:incParadox(ab.paradox or ab.sustain_paradox / 10) self:useEnergy() return false -- Now Check Anomalies elseif not game.zone.no_anomalies and not self:attr("no_paradox_fail") and self:paradoxAnomalyChance(ab.paradox or ab.sustain_paradox) then -- Random anomaly self:incParadox(ab.paradox or ab.sustain_paradox / 2) local ts = {} for id, t in pairs(self.talents_def) do if t.type[1] == "chronomancy/anomalies" then ts[#ts+1] = id end end if not silent then game.logPlayer(self, "You lose control and unleash an anomaly!") end self:forceUseTalent(rng.table(ts), {ignore_energy=true}) self:useEnergy() return false end end -- Confused ? lose a turn! if self:attr("confused") and (ab.mode ~= "sustained" or not self:isTalentActive(ab.id)) and ab.no_energy ~= true and not fake and not self:attr("force_talent_ignore_ressources") then if rng.percent(self:attr("confused")) then if not silent then game.logSeen(self, "%s is confused and fails to use %s.", self.name:capitalize(), ab.name) end self:useEnergy() return false end end -- Failure chance? if self:attr("talent_fail_chance") and (ab.mode ~= "sustained" or not self:isTalentActive(ab.id)) and ab.no_energy ~= true and not fake and not self:attr("force_talent_ignore_ressources") then if rng.percent(self:attr("talent_fail_chance")) then if not silent then game.logSeen(self, "%s fails to use %s.", self.name:capitalize(), ab.name) end self:useEnergy() return false end end -- Special checks if ab.on_pre_use and not ab.on_pre_use(self, ab, silent, fake) then return false end if not silent then -- Allow for silent talents if ab.message ~= nil then if ab.message then game.logSeen(self, "%s", self:useTalentMessage(ab)) end elseif ab.mode == "sustained" and not self:isTalentActive(ab.id) then game.logSeen(self, "%s activates %s.", self.name:capitalize(), ab.name) elseif ab.mode == "sustained" and self:isTalentActive(ab.id) then game.logSeen(self, "%s deactivates %s.", self.name:capitalize(), ab.name) elseif ab.is_spell then game.logSeen(self, "%s casts %s.", self.name:capitalize(), ab.name) else game.logSeen(self, "%s uses %s.", self.name:capitalize(), ab.name) end end -- Log vim usage for free vim on kill if not fake and ab.vim then self.last_vim_turn = game.turn self.last_vim_spent = ab.vim end return true end --- Called before a talent is used -- Check if it must use a turn, mana, stamina, ... -- @param ab the talent (not the id, the table) -- @param ret the return of the talent action -- @return true to continue, false to stop function _M:postUseTalent(ab, ret) if not ret then return end self.changed = true -- Handle inscriptions (delay it so it does not affect current inscription) game:onTickEnd(function() if ab.type[1] == "inscriptions/infusions" then self:setEffect(self.EFF_INFUSION_COOLDOWN, 10, {power=1}) elseif ab.type[1] == "inscriptions/runes" then self:setEffect(self.EFF_RUNE_COOLDOWN, 10, {power=1}) elseif ab.type[1] == "inscriptions/taints" then self:setEffect(self.EFF_TAINT_COOLDOWN, 10, {power=1}) end end) if not ab.no_energy then if ab.is_spell then self:useEnergy(game.energy_to_act * self:combatSpellSpeed()) elseif ab.type[1]:find("^technique/") then self:useEnergy(game.energy_to_act * self:combatSpeed()) else self:useEnergy() end end local trigger = false if self:attr("force_talent_ignore_ressources") then -- nothing elseif ab.mode == "sustained" then if not self:isTalentActive(ab.id) then if ab.sustain_mana then trigger = true; self:incMaxMana(-ab.sustain_mana) end if ab.sustain_stamina then trigger = true; self:incMaxStamina(-ab.sustain_stamina) end if ab.sustain_vim then trigger = true; self:incMaxVim(-ab.sustain_vim) end if ab.sustain_equilibrium then trigger = true; self:incMinEquilibrium(ab.sustain_equilibrium) end if ab.sustain_positive then trigger = true; self:incMaxPositive(-ab.sustain_positive) end if ab.sustain_negative then trigger = true; self:incMaxNegative(-ab.sustain_negative) end if ab.sustain_hate then trigger = true; self:incMaxHate(-ab.sustain_hate) end if ab.sustain_paradox then trigger = true; self:incMinParadox(ab.sustain_paradox) end if ab.sustain_psi then trigger = true; self:incMaxPsi(-ab.sustain_psi) end else if ab.sustain_mana then self:incMaxMana(ab.sustain_mana) end if ab.sustain_stamina then self:incMaxStamina(ab.sustain_stamina) end if ab.sustain_vim then self:incMaxVim(ab.sustain_vim) end if ab.sustain_equilibrium then self:incMinEquilibrium(-ab.sustain_equilibrium) end if ab.sustain_positive then self:incMaxPositive(ab.sustain_positive) end if ab.sustain_negative then self:incMaxNegative(ab.sustain_negative) end if ab.sustain_hate then self:incMaxHate(ab.sustain_hate) end if ab.sustain_paradox then self:incMinParadox(-ab.sustain_paradox) end if ab.sustain_psi then self:incMaxPsi(ab.sustain_psi) end end else if ab.mana then trigger = true; self:incMana(-util.getval(ab.mana, self, ab) * (100 + 2 * self:combatFatigue()) / 100) end if ab.stamina then trigger = true; self:incStamina(-ab.stamina * (100 + self:combatFatigue()) / 100) end -- Vim is not affected by fatigue if ab.vim then trigger = true; self:incVim(-ab.vim) end if ab.positive then trigger = true; self:incPositive(-ab.positive * (100 + self:combatFatigue()) / 100) end if ab.negative then trigger = true; self:incNegative(-ab.negative * (100 + self:combatFatigue()) / 100) end if ab.hate then trigger = true; self:incHate(-ab.hate) end -- Equilibrium is not affected by fatigue if ab.equilibrium then trigger = true; self:incEquilibrium(ab.equilibrium) end -- Paradox is not affected by fatigue but it's cost does increase exponentially if ab.paradox then trigger = true; self:incParadox(ab.paradox * (1 + (self.paradox / 300))) end if ab.psi then trigger = true; self:incPsi(-ab.psi * (100 + 2 * self.fatigue) / 100) end end if trigger and self:hasEffect(self.EFF_BURNING_HEX) then local p = self:hasEffect(self.EFF_BURNING_HEX) DamageType:get(DamageType.FIRE).projector(p.src, self.x, self.y, DamageType.FIRE, p.dam) end -- Cancel stealth! if ab.id ~= self.T_STEALTH and ab.id ~= self.T_HIDE_IN_PLAIN_SIGHT and not ab.no_break_stealth then self:breakStealth() end if ab.id ~= self.T_LIGHTNING_SPEED then self:breakLightningSpeed() end if ab.id ~= self.T_GATHER_THE_THREADS then self:breakGatherTheThreads() end self:breakStepUp() return true end --- Force a talent to activate without using energy or such function _M:forceUseTalent(t, def) if def.no_equilibrium_fail then self:attr("no_equilibrium_fail", 1) end if def.no_paradox_fail then self:attr("no_paradox_fail", 1) end local ret = {engine.interface.ActorTalents.forceUseTalent(self, t, def)} if def.no_equilibrium_fail then self:attr("no_equilibrium_fail", -1) end if def.no_paradox_fail then self:attr("no_paradox_fail", -1) end return unpack(ret) end --- Breaks stealth if active function _M:breakStealth() if self:isTalentActive(self.T_STEALTH) then local chance = 0 if self:knowTalent(self.T_UNSEEN_ACTIONS) then chance = 10 + self:getTalentLevel(self.T_UNSEEN_ACTIONS) * 9 + (self:getLck() - 50) * 0.2 end -- Do not break stealth if rng.percent(chance) then return end self:forceUseTalent(self.T_STEALTH, {ignore_energy=true}) self.changed = true end end --- Breaks step up if active function _M:breakStepUp() if self:hasEffect(self.EFF_STEP_UP) then self:removeEffect(self.EFF_STEP_UP) end if self:hasEffect(self.EFF_WILD_SPEED) then self:removeEffect(self.EFF_WILD_SPEED) end if self:hasEffect(self.EFF_REFLEXIVE_DODGING) then self:removeEffect(self.EFF_REFLEXIVE_DODGING) end end --- Breaks lightning speed if active function _M:breakLightningSpeed() if self:hasEffect(self.EFF_LIGHTNING_SPEED) then self:removeEffect(self.EFF_LIGHTNING_SPEED) end end --- Breaks gather the threads if active function _M:breakGatherTheThreads() if self:hasEffect(self.EFF_GATHER_THE_THREADS) then self:removeEffect(self.EFF_GATHER_THE_THREADS) end end --- Return the full description of a talent -- You may overload it to add more data (like power usage, ...) function _M:getTalentFullDescription(t, addlevel) local old = self.talents[t.id] self.talents[t.id] = (self.talents[t.id] or 0) + (addlevel or 0) local d = tstring{} d:add({"color",0x6f,0xff,0x83}, "Effective talent level: ", {"color",0x00,0xFF,0x00}, ("%.1f"):format(self:getTalentLevel(t)), true) if t.mode == "passive" then d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Passive", true) elseif t.mode == "sustained" then d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Sustained", true) else d:add({"color",0x6f,0xff,0x83}, "Use mode: ", {"color",0x00,0xFF,0x00}, "Activated", true) end if t.mana or t.sustain_mana then d:add({"color",0x6f,0xff,0x83}, "Mana cost: ", {"color",0x7f,0xff,0xd4}, ""..(util.getval(t.sustain_mana or t.mana, self, t) * (100 + 2 * self:combatFatigue()) / 100), true) end if t.stamina or t.sustain_stamina then d:add({"color",0x6f,0xff,0x83}, "Stamina cost: ", {"color",0xff,0xcc,0x80}, ""..(t.sustain_stamina or t.stamina * (100 + self:combatFatigue()) / 100), true) end if t.equilibrium or t.sustain_equilibrium then d:add({"color",0x6f,0xff,0x83}, "Equilibrium cost: ", {"color",0x00,0xff,0x74}, ""..(t.equilibrium or t.sustain_equilibrium), true) end if t.vim or t.sustain_vim then d:add({"color",0x6f,0xff,0x83}, "Vim cost: ", {"color",0x88,0x88,0x88}, ""..(t.sustain_vim or t.vim), true) end if t.positive or t.sustain_positive then d:add({"color",0x6f,0xff,0x83}, "Positive energy cost: ", {"color",255, 215, 0}, ""..(t.sustain_positive or t.positive * (100 + self:combatFatigue()) / 100), true) end if t.negative or t.sustain_negative then d:add({"color",0x6f,0xff,0x83}, "Negative energy cost: ", {"color", 127, 127, 127}, ""..(t.sustain_negative or t.negative * (100 + self:combatFatigue()) / 100), true) end if t.hate or t.sustain_hate then d:add({"color",0x6f,0xff,0x83}, "Hate cost: ", {"color", 127, 127, 127}, ""..(t.hate or t.sustain_hate), true) end if t.paradox or t.sustain_paradox then d:add({"color",0x6f,0xff,0x83}, "Paradox cost: ", {"color", 176, 196, 222}, ("%0.2f"):format(t.sustain_paradox or t.paradox * (1 + (self.paradox / 300))), true) end if t.psi or t.sustain_psi then d:add({"color",0x6f,0xff,0x83}, "Psi cost: ", {"color",0x7f,0xff,0xd4}, ""..(t.sustain_psi or t.psi * (100 + 2 * self.fatigue) / 100), true) end if self:getTalentRange(t) > 1 then d:add({"color",0x6f,0xff,0x83}, "Range: ", {"color",0xFF,0xFF,0xFF}, ("%0.2f"):format(self:getTalentRange(t)), true) else d:add({"color",0x6f,0xff,0x83}, "Range: ", {"color",0xFF,0xFF,0xFF}, "melee/personal", true) end if self:getTalentCooldown(t) then d:add({"color",0x6f,0xff,0x83}, "Cooldown: ", {"color",0xFF,0xFF,0xFF}, ""..self:getTalentCooldown(t), true) end local speed = self:getTalentProjectileSpeed(t) if speed then d:add({"color",0x6f,0xff,0x83}, "Travel Speed: ", {"color",0xFF,0xFF,0xFF}, ""..(speed * 100).."% of base", true) else d:add({"color",0x6f,0xff,0x83}, "Travel Speed: ", {"color",0xFF,0xFF,0xFF}, "instantaneous", true) end local uspeed = "1 turn" if t.no_energy and type(t.no_energy) == "boolean" and t.no_energy == true then uspeed = "instant" end d:add({"color",0x6f,0xff,0x83}, "Usage Speed: ", {"color",0xFF,0xFF,0xFF}, uspeed, true) d:add({"color",0x6f,0xff,0x83}, "Description: ", {"color",0xFF,0xFF,0xFF}) d:merge(t.info(self, t):toTString():tokenize(" ()[]")) d:add(true) self.talents[t.id] = old return d end function _M:getTalentCooldown(t) if not t.cooldown then return end local cd = t.cooldown if type(cd) == "function" then cd = cd(self, t) end if not cd then return end if t.type[1] == "inscriptions/infusions" then local eff = self:hasEffect(self.EFF_INFUSION_COOLDOWN) if eff and eff.power then cd = cd + eff.power end elseif t.type[1] == "inscriptions/runes" then local eff = self:hasEffect(self.EFF_RUNE_COOLDOWN) if eff and eff.power then cd = cd + eff.power end elseif t.type[1] == "inscriptions/taints" then local eff = self:hasEffect(self.EFF_TAINT_COOLDOWN) if eff and eff.power then cd = cd + eff.power end end if self.talent_cd_reduction[t.id] then cd = cd - self.talent_cd_reduction[t.id] end if self.talent_cd_reduction.all then cd = cd - self.talent_cd_reduction.all end if t.is_spell then return math.ceil(cd * (1 - self.spell_cooldown_reduction or 0)) else return cd end end --- Starts a talent cooldown; overloaded from the default to handle talent cooldown reduction -- @param t the talent to cooldown function _M:startTalentCooldown(t) if not t.cooldown then return end self.talents_cd[t.id] = self:getTalentCooldown(t) self.changed = true end --- Setups a talent automatic use function _M:checkSetTalentAuto(tid, v) local t = self:getTalentFromId(tid) if v then local doit = function() self:setTalentAuto(tid, true) Dialog:simplePopup("Automatic use enabled", t.name:capitalize().." will now be used as often as possible automatically.") end local list = {} if t.no_energy ~= true then list[#list+1] = "- requires a turn to use" end if t.requires_target then list[#list+1] = "- requires a target, your last hostile one will be automatically used" end if t.auto_use_warning then list[#list+1] = t.auto_use_warning end if #list == 0 then doit() else Dialog:yesnoLongPopup("Automatic use", t.name:capitalize()..":\n"..table.concat(list, "\n").."\n Are you sure?", 500, function(ret) if ret then doit() end end) end else self:setTalentAuto(tid, false) Dialog:simplePopup("Automatic use disabled", t.name:capitalize().." will not be automatically used.") end end --- How much experience is this actor worth -- @param target to whom is the exp rewarded -- @return the experience rewarded function _M:worthExp(target) if not target.level or self.level < target.level - 7 then return 0 end -- HHHHAACKKK ! Use a normal scheme for the game except in the infinite dungeon if not game.zone.infinite_dungeon then local mult = 0.6 if self.rank == 1 then mult = 0.6 elseif self.rank == 2 then mult = 0.8 elseif self.rank == 3 then mult = 3 elseif self.rank == 3.5 then mult = 12 elseif self.rank == 4 then mult = 45 elseif self.rank >= 5 then mult = 100 end return self.level * mult * self.exp_worth * (target.exp_kill_multiplier or 1) else local mult = 2 + (self.exp_kill_multiplier or 0) if self.rank == 1 then mult = 2 elseif self.rank == 2 then mult = 2 elseif self.rank == 3 then mult = 3.5 elseif self.rank == 3.5 then mult = 5 elseif self.rank == 4 then mult = 6 elseif self.rank >= 5 then mult = 6.5 end return self.level * mult * self.exp_worth * (target.exp_kill_multiplier or 1) end end --- Suffocate a bit, lose air function _M:suffocate(value, src, death_message) if self:attr("no_breath") then return false, false end if self:attr("invulnerable") then return false, false end self.air = self.air - value local ae = game.level.map(self.x, self.y, Map.ACTOR) if self.air <= 0 then game.logSeen(self, "%s suffocates to death!", self.name:capitalize()) return self:die(src, {special_death_msg=death_message or "suffocated to death"}), true end return false, true end --- Can the actor see the target actor -- This does not check LOS or such, only the actual ability to see it.
-- Check for telepathy, invisibility, stealth, ... function _M:canSeeNoCache(actor, def, def_pct) if not actor then return false, 0 end -- Full ESP if self.esp_all and self.esp_all > 0 then return true, 100 end -- ESP, see all, or only types/subtypes if self.esp then local esp = self.esp -- Type based ESP if esp[actor.type] and esp[actor.type] > 0 then return true, 100 end if esp[actor.type.."/"..actor.subtype] and esp[actor.type.."/"..actor.subtype] > 0 then return true, 100 end end -- Blindness means can't see anything if self:attr("blind") then return false, 0 end -- Check for stealth. Checks against the target cunning and level if actor:attr("stealth") and actor ~= self then local def = self.level / 2 + self:getCun(25, true) + (self:attr("see_stealth") or 0) local hit, chance = self:checkHit(def, actor:attr("stealth") + (actor:attr("inc_stealth") or 0), 0, 100) if not hit then return false, chance end end -- check if the actor is stalking you if self.stalker then if self.stalker == actor then return false, 0 end end -- Check for invisibility. This is a "simple" checkHit between invisible and see_invisible attrs if actor:attr("invisible") then -- Special case, 0 see invisible, can NEVER see invisible things if not self:attr("see_invisible") then return false, 0 end local hit, chance = self:checkHit(self:attr("see_invisible"), actor:attr("invisible"), 0, 100) if not hit then return false, chance end end -- check cursed pity talent if actor:knowTalent(self.T_PITY) then local t = actor:getTalentFromId(self.T_PITY) if math.floor(core.fov.distance(self.x, self.y, actor.x, actor.y)) >= actor:getTalentRange(t) then return false, 50 - actor:getTalentLevel(self.T_PITY) * 5 end end if def ~= nil then return def, def_pct else return true, 100 end end function _M:canSee(actor, def, def_pct) if not actor then return false, 0 end self.can_see_cache = self.can_see_cache or {} local s = tostring(def).."/"..tostring(def_pct) if self.can_see_cache[actor] and self.can_see_cache[actor][s] then return self.can_see_cache[actor][s][1], self.can_see_cache[actor][s][2] end self.can_see_cache[actor] = self.can_see_cache[actor] or {} self.can_see_cache[actor][s] = self.can_see_cache[actor][s] or {} local res, chance = self:canSeeNoCache(actor, def, def_pct) self.can_see_cache[actor][s] = {res,chance} -- Make sure the display updates if self.player and type(def) == "nil" and actor._mo then actor._mo:onSeen(res) end return res, chance end --- Reset our own seeing cache function _M:resetCanSeeCache() self.can_see_cache = {} setmetatable(self.can_see_cache, {__mode="k"}) end --- Reset the cache of everything else that had see us on the level function _M:resetCanSeeCacheOf() if not game.level then return end for uid, e in pairs(game.level.entities) do if e.can_see_cache and e.can_see_cache[self] then e.can_see_cache[self] = nil end end game.level.map:updateMap(self.x, self.y) end --- Does the actor have LOS to the target function _M:hasLOS(x, y, what) if not x or not y then return false, self.x, self.y end what = what or "block_sight" local lx, ly if what == "block_sight" then local darkVisionRange if self:knowTalent(self.T_DARK_VISION) then local t = self:getTalentFromId(self.T_DARK_VISION) darkVisionRange = self:getTalentRange(t) end local l = line.new(self.x, self.y, x, y) local inCreepingDark, lastX, lastY = false lx, ly = l() while lx and ly do if game.level.map:checkAllEntities(lx, ly, "block_sight") then if darkVisionRange and game.level.map:checkAllEntities(lx, ly, "creepingDark") then inCreepingDark = true else break end end if inCreepingDark and darkVisionRange and core.fov.distance(self.x, self.y, lx, ly) > darkVisionRange then lx, ly = lastX or lx, lastY or ly break end lastX, lastY = lx, ly lx, ly = l() end else local l = line.new(self.x, self.y, x, y) lx, ly = l() while lx and ly do if game.level.map:checkAllEntities(lx, ly, what) then break end lx, ly = l() end end -- Ok if we are at the end reset lx and ly for the next code if not lx and not ly then lx, ly = x, y end if lx == x and ly == y then return true, lx, ly end return false, lx, ly end --- Can the target be applied some effects -- @param what a string describing what is being tried function _M:canBe(what) if what == "poison" and rng.percent(100 * (self:attr("poison_immune") or 0)) then return false end if what == "disease" and rng.percent(100 * (self:attr("disease_immune") or 0)) then return false end if what == "cut" and rng.percent(100 * (self:attr("cut_immune") or 0)) then return false end if what == "confusion" and rng.percent(100 * (self:attr("confusion_immune") or 0)) then return false end if what == "blind" and rng.percent(100 * (self:attr("blind_immune") or 0)) then return false end if what == "silence" and rng.percent(100 * (self:attr("silence_immune") or 0)) then return false end if what == "disarm" and rng.percent(100 * (self:attr("disarm_immune") or 0)) then return false end if what == "pin" and rng.percent(100 * (self:attr("pin_immune") or 0)) and not self:attr("levitation") then return false end if what == "stun" and rng.percent(100 * (self:attr("stun_immune") or 0)) then return false end if what == "fear" and rng.percent(100 * (self:attr("fear_immune") or 0)) then return false end if what == "knockback" and (rng.percent(100 * (self:attr("knockback_immune") or 0)) or self:attr("never_move")) then return false end if what == "stone" and rng.percent(100 * (self:attr("stone_immune") or 0)) then return false end if what == "instakill" and rng.percent(100 * (self:attr("instakill_immune") or 0)) then return false end if what == "teleport" and (rng.percent(100 * (self:attr("teleport_immune") or 0)) or self:attr("encased_in_ice")) then return false end if what == "worldport" and game.level.data and game.level.data.no_worldport then return false end if what == "summon" and self:attr("suppress_summon") then return false end return true end --- Adjusts timed effect durations based on rank and other things function _M:updateEffectDuration(dur, what) -- Rank reduction: below elite = none; elite = 1, boss = 2, elite boss = 3 local rankmod = 0 if self.rank == 3 then rankmod = 25 elseif self.rank == 3.5 then rankmod = 40 elseif self.rank == 4 then rankmod = 45 elseif self.rank == 5 then rankmod = 75 end if rankmod <= 0 then return dur end print("Effect duration reduction <", dur) if what == "stun" then local p = self:combatPhysicalResist(), rankmod * (util.bound(self:combatPhysicalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "pin" then local p = self:combatPhysicalResist(), rankmod * (util.bound(self:combatPhysicalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "disarm" then local p = self:combatPhysicalResist(), rankmod * (util.bound(self:combatPhysicalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "frozen" then local p = self:combatSpellResist(), rankmod * (util.bound(self:combatSpellResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "blind" then local p = self:combatMentalResist(), rankmod * (util.bound(self:combatMentalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "silence" then local p = self:combatMentalResist(), rankmod * (util.bound(self:combatMentalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "slow" then local p = self:combatPhysicalResist(), rankmod * (util.bound(self:combatPhysicalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) elseif what == "confusion" then local p = self:combatMentalResist(), rankmod * (util.bound(self:combatMentalResist() * 3, 40, 115) / 100) dur = dur - math.ceil(dur * (p) / 100) end print("Effect duration reduction >", dur) return dur end --- Adjust temporary effects function _M:on_set_temporary_effect(eff_id, e, p) if e.status == "detrimental" and self:knowTalent(self.T_RESILIENT_BONES) then p.dur = math.ceil(p.dur * (1 - (self:getTalentLevel(self.T_RESILIENT_BONES) / 12))) end if e.status == "detrimental" and self:attr("negative_status_effect_immune") then p.dur = 0 end end --- Called when we are projected upon -- This is used to do spell reflection, antimagic, ... function _M:on_project(tx, ty, who, t, x, y, damtype, dam, particles) -- Spell reflect if self:attr("spell_reflect") and ((t.talent and t.talent.reflectable) or t.reflectable) and rng.percent(self:attr("spell_reflect")) then game.logSeen(self, "%s reflects the spell!", self.name:capitalize()) -- Setup the bypass so it does not eternally reflect between two actors t.bypass = true who:project(t, x, y, damtype, dam, particles) return true end -- Spell absorb if self:attr("spell_absorb") and (t.talent and t.talent.is_spell) and rng.percent(self:attr("spell_absorb")) then game.logSeen(self, "%s ignores the spell!", self.name:capitalize()) return true end return false end --- Called when we have been projected upon and the DamageType is about to be called function _M:projected(tx, ty, who, t, x, y, damtype, dam, particles) return false end --- Called when we are targeted by a projectile function _M:on_projectile_target(x, y, p) if self:attr("slow_projectiles") then print("Projectile slowing down from", p.energy.mod) p.energy.mod = p.energy.mod * (100 - self.slow_projectiles) / 100 print("Projectile slowing down to", p.energy.mod) end if self:knowTalent(self.T_HEIGHTENED_REFLEXES) then local t = self:getTalentFromId(self.T_HEIGHTENED_REFLEXES) t.do_reflexes(self, t) end end --- Called when we have acquired grids function _M:on_project_grids(grids) if self:attr("encased_in_ice") then -- Only hit yourself while next(grids) do grids[next(grids)] = nil end grids[self.x] = {[self.y]=true} end end --- Call when added to a level -- Used to make escorts and such function _M:addedToLevel(level, x, y) if not self._rst_full then self:resetToFull() self._rst_full = true end -- Only do it once, the first time we come into being self:updateModdableTile() if self.make_escort then for _, filter in ipairs(self.make_escort) do for i = 1, filter.number do if not filter.chance or rng.percent(filter.chance) then -- Find space local x, y = util.findFreeGrid(self.x, self.y, 10, true, {[Map.ACTOR]=true}) if not x then break end -- Find an actor with that filter local m = game.zone:makeEntity(game.level, "actor", filter, nil, true) if m and m:canMove(x, y) then if filter.no_subescort then m.make_escort = nil end game.zone:addEntity(game.level, m, "actor", x, y) elseif m then m:removed() end end end end self.make_escort = nil end self:check("on_added_to_level", level, x, y) end