Skip to content
Snippets Groups Projects
Actor.lua 60.8 KiB
Newer Older
dg's avatar
dg committed
-- ToME - Tales of Maj'Eyal
dg's avatar
dg committed
-- Copyright (C) 2009, 2010 Nicolas Casalini
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org

dg's avatar
dg committed
require "engine.class"
require "engine.Actor"
dg's avatar
dg committed
require "engine.Autolevel"
dg's avatar
dg committed
require "engine.interface.ActorInventory"
dg's avatar
dg committed
require "engine.interface.ActorTemporaryEffects"
dg's avatar
dg committed
require "engine.interface.ActorLife"
require "engine.interface.ActorProject"
dg's avatar
dg committed
require "engine.interface.ActorLevel"
dg's avatar
dg committed
require "engine.interface.ActorStats"
dg's avatar
dg committed
require "engine.interface.ActorTalents"
dg's avatar
dg committed
require "engine.interface.ActorResource"
dg's avatar
dg committed
require "engine.interface.ActorQuest"
dg's avatar
dg committed
require "engine.interface.BloodyDeath"
dg's avatar
dg committed
require "engine.interface.ActorFOV"
dg's avatar
dg committed
require "mod.class.interface.Combat"
require "mod.class.interface.Archery"
dg's avatar
dg committed
local Faction = require "engine.Faction"
dg's avatar
dg committed
local Map = require "engine.Map"
local DamageType = require "engine.DamageType"
dg's avatar
dg committed

dg's avatar
dg committed
module(..., package.seeall, class.inherit(
	-- a ToME actor is a complex beast it uses may inetrfaces
	engine.Actor,
dg's avatar
dg committed
	engine.interface.ActorInventory,
dg's avatar
dg committed
	engine.interface.ActorTemporaryEffects,
dg's avatar
dg committed
	engine.interface.ActorLife,
	engine.interface.ActorProject,
dg's avatar
dg committed
	engine.interface.ActorLevel,
dg's avatar
dg committed
	engine.interface.ActorStats,
dg's avatar
dg committed
	engine.interface.ActorTalents,
dg's avatar
dg committed
	engine.interface.ActorResource,
dg's avatar
dg committed
	engine.interface.ActorQuest,
dg's avatar
dg committed
	engine.interface.BloodyDeath,
dg's avatar
dg committed
	engine.interface.ActorFOV,
	mod.class.interface.Combat,
	mod.class.interface.Archery
dg's avatar
dg committed
))
dg's avatar
dg committed

-- Dont save the can_see_cache
_M._no_save_fields.can_see_cache = true

-- Use distance maps
_M.__do_distance_map = true

dg's avatar
dg committed
function _M:init(t, no_default)
dg's avatar
dg committed
	-- Define some basic combat stats
	self.combat_def = 0
	self.combat_armor = 0
	self.combat_atk = 0
	self.combat_apr = 0
	self.combat_dam = 0
	self.combat_physcrit = 0
	self.combat_physspeed = 0
	self.combat_spellspeed = 0
	self.combat_spellcrit = 0
	self.combat_spellpower = 0
	self.combat_mindpower = 0
dg's avatar
dg committed

	self.combat_physresist = 0
	self.combat_spellresist = 0
dg's avatar
dg committed
	self.combat_mentalresist = 0
dg's avatar
dg committed

dg's avatar
dg committed
	self.fatigue = 0

dg's avatar
dg committed
	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
dg's avatar
dg committed
	self.unused_talents_types = self.unused_talents_types or 0

dg's avatar
dg committed
	t.healing_factor = t.healing_factor or 1

dg's avatar
dg committed
	t.sight = t.sight or 20

dg's avatar
dg committed
	t.resource_pool_refs = t.resource_pool_refs or {}
dg's avatar
dg committed

	t.lite = t.lite or 0

dg's avatar
dg committed
	t.size_category = t.size_category or 3
	t.rank = t.rank or 2

dg's avatar
dg committed
	t.life_rating = t.life_rating or 10
	t.mana_rating = t.mana_rating or 4
dg's avatar
dg committed
	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
dg's avatar
dg committed

dg's avatar
dg committed
	t.esp = t.esp or {range=10}
dg's avatar
dg committed

dg's avatar
dg committed
	t.talent_cd_reduction = t.talent_cd_reduction or {}

dg's avatar
dg committed
	t.on_melee_hit = t.on_melee_hit or {}
dg's avatar
dg committed
	t.melee_project = t.melee_project or {}
dg's avatar
dg committed
	t.ranged_project = t.ranged_project or {}
dg's avatar
dg committed
	t.can_pass = t.can_pass or {}
	t.move_project = t.move_project or {}
	t.can_breath = t.can_breath or {}
dg's avatar
dg committed

dg's avatar
dg committed
	-- Resistances
	t.resists = t.resists or {}
dg's avatar
dg committed
	t.resists_pen = t.resists_pen or {}
dg's avatar
dg committed

dg's avatar
dg committed
	-- % Increase damage
	t.inc_damage = t.inc_damage or {}

dg's avatar
dg committed
	-- Default regen
	t.air_regen = t.air_regen or 3
dg's avatar
dg committed
	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
dg's avatar
dg committed
	t.equilibrium_regen = t.equilibrium_regen or 0 -- Equilibrium does not regen
dg's avatar
dg committed
	t.vim_regen = t.vim_regen or 0 -- Vim does not regen
dg's avatar
dg committed
	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
dg's avatar
dg committed

	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
dg's avatar
dg committed

dg's avatar
dg committed
	t.hate_rating = t.hate_rating or 0.2
	t.hate_regen = t.hate_regen or -0.035
	t.max_hate = t.max_hate or 10
	t.absolute_max_hate = t.absolute_max_hate or 15
	t.hate = t.hate or 10
	t.hate_per_kill = t.hate_per_kill or 0.8

dg's avatar
dg committed
	-- Equilibrium has a default very high max, as bad effects happen even before reaching it
	t.max_equilibrium = t.max_equilibrium or 100000
dg's avatar
dg committed
	t.equilibrium = t.equilibrium or 0
dg's avatar
dg committed

dg's avatar
dg committed
	t.money = t.money or 0

dg's avatar
dg committed
	-- Default melee barehanded damage
	self.combat = { dam=1, atk=1, apr=0, dammod={str=1} }

dg's avatar
dg committed
	engine.Actor.init(self, t, no_default)
dg's avatar
dg committed
	engine.interface.ActorInventory.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorTemporaryEffects.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorLife.init(self, t)
	engine.interface.ActorProject.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorTalents.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorResource.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorStats.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorLevel.init(self, t)
dg's avatar
dg committed
	engine.interface.ActorFOV.init(self, t)

	self:resetCanSeeCache()
dg's avatar
dg committed
end

dg's avatar
dg committed
function _M:act()
dg's avatar
dg committed
	if not engine.Actor.act(self) then return end

	-- If ressources are too low, disable sustains
	if self.mana < 1 or self.stamina < 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})
dg's avatar
dg committed
	-- Cooldown talents
	self:cooldownTalents()
dg's avatar
dg committed
	-- Regen resources
	self:regenLife()
dg's avatar
dg committed
	if self:knowTalent(self.T_UNNATURAL_BODY) then
		local t = self:getTalentFromId(self.T_UNNATURAL_BODY)
		t.do_regenLife(self, t)
	end
dg's avatar
dg committed
	self:regenResources()
dg's avatar
dg committed
	-- Compute timed effects
	self:timedEffects()
dg's avatar
dg committed

dg's avatar
dg committed
	-- Handle thunderstorm, even if the actor is stunned or incampacited it still works
	if self:isTalentActive(self.T_THUNDERSTORM) then
		local t = self:getTalentFromId(self.T_THUNDERSTORM)
		t.do_storm(self, t)
	end
dg's avatar
dg committed
	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
dg's avatar
dg committed
	-- 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
dg's avatar
dg committed

dg's avatar
dg committed
	if self:attr("stunned") then
		self.stunned_counter = (self.stunned_counter or 0) + (self:attr("stun_immune") or 0) * 100
		if self.stunned_counter < 100 then
			self.energy.value = 0
		else
			-- We are saved for this turn
			self.stunned_counter = self.stunned_counter - 100
			game.logSeen(self, "%s temporarily fights the stun.", self.name:capitalize())
		end
	end
	if self:attr("encased_in_ice") then self.energy.value = 0 end
	if self:attr("stoned") then self.energy.value = 0 end
dg's avatar
dg committed
	if self:attr("dazed") then self.energy.value = 0 end
dg's avatar
dg committed

	-- 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] then self:suffocate(-air_level, self) end
	end
dg's avatar
dg committed
	-- 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

dg's avatar
dg committed
	-- Still enough energy to act ?
	if self.energy.value < game.energy_to_act then return false end

dg's avatar
dg committed
	-- 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

dg's avatar
dg committed
	return true
dg's avatar
dg committed
end

dg's avatar
dg committed
function _M:move(x, y, force)
	local moved = false
dg's avatar
dg committed

dg's avatar
dg committed
	if force or self:enoughEnergy() then
dg's avatar
dg committed
		-- Confused ?
		if not force and self:attr("confused") then
dg's avatar
dg committed
			if rng.percent(self:attr("confused")) then
dg's avatar
dg committed
				x, y = self.x + rng.range(-1, 1), self.y + rng.range(-1, 1)
			end
		end

dg's avatar
dg committed
		-- Should we prob travel through walls ?
		if not force and self:attr("prob_travel") and game.level.map:checkEntity(x, y, Map.TERRAIN, "block_move", self) then
dg's avatar
dg committed
			moved = self:probabilityTravel(x, y, self:attr("prob_travel"))
dg's avatar
dg committed
		-- Never move but tries to attack ? ok
dg's avatar
dg committed
		elseif not force and self:attr("never_move") then
dg's avatar
dg committed
			-- 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
dg's avatar
dg committed
		else
dg's avatar
dg committed
			moved = engine.Actor.move(self, x, y, force)
dg's avatar
dg committed
		end
dg's avatar
dg committed
		if not force and moved and not self.did_energy then self:useEnergy(game.energy_to_act * self:combatMovementSpeed()) end
dg's avatar
dg committed
	end
dg's avatar
dg committed
	self.did_energy = nil
dg's avatar
dg committed

	-- Try to detect traps
	if self:knowTalent(self.T_TRAP_DETECTION) then
		local power = self:getTalentLevel(self.T_TRAP_DETECTION) * self:getCun(25)
		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:isTalentActive(self.T_BODY_OF_STONE) then
		self:forceUseTalent(self.T_BODY_OF_STONE, {ignore_energy=true})
	end

dg's avatar
dg committed
	return moved
end

dg's avatar
dg committed
--- Get the "path string" for this actor
-- See Map:addPathString() for more info
function _M:getPathString()
dg's avatar
dg committed
	local ps = self.open_door and "return {open_door=true,can_pass={" or "return {can_pass={"
dg's avatar
dg committed
	for what, check in pairs(self.can_pass) do
		ps = ps .. what.."="..check..","
	end
dg's avatar
dg committed
	ps = ps.."}}"
--	print("[PATH STRING] for", self.name, " :=: ", ps)
dg's avatar
dg committed
	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 immunte to the teleportation and drops to the floor!", o:getName{do_color=true})
			end
		end
	end
end

dg's avatar
dg committed
--- Blink through walls
dg's avatar
dg committed
function _M:probabilityTravel(x, y, dist)
	if game.zone.wilderness then return true end

dg's avatar
dg committed
	local dirx, diry = x - self.x, y - self.y
	local tx, ty = x, y
dg's avatar
dg committed
	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
dg's avatar
dg committed
		tx = tx + dirx
		ty = ty + diry
dg's avatar
dg committed
		dist = dist - 1
dg's avatar
dg committed
	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()
dg's avatar
dg committed
		return engine.Actor.move(self, tx, ty, false)
dg's avatar
dg committed
	end
	return true
end

dg's avatar
dg committed
--- 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 teleporatation
-- @param y the coord of the teleporatation
-- @param dist the radius of the random effect, if set to 0 it is a precise teleport
-- @param min_dist the minimun 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 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
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Quake a zone
-- Moves randomly each grid to an other grid
dg's avatar
dg committed
function _M:doQuake(tg, x, y)
dg's avatar
dg committed
	local w = game.level.map.w
dg's avatar
dg committed
	local locs = {}
	local ms = {}
	self:project(tg, x, y, function(tx, ty)
dg's avatar
dg committed
		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
dg's avatar
dg committed
	end)

	while #locs > 0 do
		local l = rng.tableRemove(locs)
		local m = rng.tableRemove(ms)
dg's avatar
dg committed

		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) do
			if e.move then
				e.x = nil e.y = nil e:move(l.x, l.y, true)
dg's avatar
dg committed
			end
dg's avatar
dg committed
		end
	end
	game.level.map:cleanFOV()
	game.level.map.changed = true
dg's avatar
dg committed
	game.level.map:redisplay()
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Reveals location surrounding the actor
dg's avatar
dg committed
function _M:magicMap(radius, x, y)
	x = x or self.x
	y = y or self.y
dg's avatar
dg committed
	radius = math.floor(radius)
dg's avatar
dg committed
	local ox, oy

	self.x, self.y, ox, oy = x, y, self.x, self.y
	self:computeFOV(radius, "block_sense", function(x, y)
dg's avatar
dg committed
		game.level.map.remembers(x, y, true)
		game.level.map.has_seens(x, y, true)
	end, true, true, true)
dg's avatar
dg committed

	self.x, self.y = ox, oy
dg's avatar
dg committed
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

dg's avatar
dg committed
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
dg's avatar
dg committed
	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

dg's avatar
dg committed
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 + 0.3)
dg's avatar
dg committed
	elseif self.rank == 4 then return value * (level_adjust + 0.3)
	elseif self.rank >= 5 then return value * (level_adjust + 0.5)
dg's avatar
dg committed
	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

dg's avatar
dg committed
function _M:TextRank()
	local rank, color = "normal", "#ANTIQUE_WHITE#"
	if self.rank == 1 then rank, color = "critter", "#C0C0C0#"
dg's avatar
dg committed
	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#"
dg's avatar
dg committed
	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"
dg's avatar
dg committed
	elseif self.size_category == 5 then sizecat = "huge"
	elseif self.size_category >= 6 then sizecat = "gargantuan"
dg's avatar
dg committed
	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", self:reactionToward(game.player)
	if factlevel < 0 then factcolor, factstate = "#LIGHT_RED#", "hostile"
	elseif factlevel > 0 then factcolor, factstate = "#LIGHT_GREEN#", "friendly"
dg's avatar
dg committed
	end

dg's avatar
dg committed
	local rank, rank_color = self:TextRank()
dg's avatar
dg committed

	local effs = {}
	for tid, act in pairs(self.sustain_talents) do
		if act then effs[#effs+1] = ("- #LIGHT_GREEN#%s"):format(self:getTalentFromId(tid).name) end
	end
	for eff_id, p in pairs(self.tmp) do
		local e = self.tempeffect_def[eff_id]
		if e.status == "detrimental" then
			effs[#effs+1] = ("- #LIGHT_RED#%s"):format(e.desc)
		else
			effs[#effs+1] = ("- #LIGHT_GREEN#%s"):format(e.desc)
		end
	end

	local resists = {}
	for t, v in pairs(self.resists) do
dg's avatar
dg committed
		resists[#resists+1] = string.format("%d%% %s", v, t == "all" and "all" or DamageType:get(t).name)
%s / %s
dg's avatar
dg committed
Rank: %s%s
dg's avatar
dg committed
#00ffff#Level: %d
Exp: %d/%d
dg's avatar
dg committed
#ff0000#HP: %d (%d%%)
dg's avatar
dg committed
Stats: %d /  %d / %d / %d / %d / %d
Resists: %s
dg's avatar
dg committed
Size: #ANTIQUE_WHITE#%s
dg's avatar
dg committed
%s
dg's avatar
dg committed
%s]]):format(
	self:getDisplayString(), rank_color, self.name,
	self.type:capitalize(), self.subtype:capitalize(),
dg's avatar
dg committed
	rank_color, rank,
dg's avatar
dg committed
	self.level,
	self.exp,
	self:getExpChart(self.level+1) or "---",
dg's avatar
dg committed
	self.life, self.life * 100 / self.max_life,
dg's avatar
dg committed
	self:getStr(),
	self:getDex(),
	self:getMag(),
	self:getWil(),
	self:getCun(),
	self:getCon(),
	table.concat(resists, ','),
dg's avatar
dg committed
	self:TextSizeCategory(),
dg's avatar
dg committed
	self.desc or "",
	factcolor, Faction.factions[self.faction].name, factstate, factlevel,
dg's avatar
dg committed
	table.concat(effs, "\n")
dg's avatar
dg committed
	)
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Called before healing
function _M:onHeal(value, src)
	if self:hasEffect(self.EFF_UNSTOPPABLE) then
		return 0
	end
dg's avatar
dg committed
	return value * (self.healing_factor or 1)
dg's avatar
dg committed
--- Called before taking a hit, it's the chance to check for shields
function _M:onTakeHit(value, src)
dg's avatar
dg committed
	-- Un-daze
	if self:hasEffect(self.EFF_DAZED) then
		self:removeEffect(self.EFF_DAZED)
	end

dg's avatar
dg committed
	-- 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

dg's avatar
dg committed
	if self:attr("invulnerable") then
		return 0
	end

	if self:attr("disruption_shield") then
dg's avatar
dg committed
		local mana = self:getMana()
		local mana_val = value * self:attr("disruption_shield")
dg's avatar
dg committed
		-- We have enough to absord the full hit
		if mana_val <= mana then
			self:incMana(-mana_val)
			self.disruption_shield_absorb = self.disruption_shield_absorb + value
dg's avatar
dg committed
			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!
dg's avatar
dg committed
			game.logSeen(self, "%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
		if value <= self.time_shield_absorb then
			self.time_shield_absorb = self.time_shield_absorb - value
			value = 0
		else
			self.time_shield_absorb = 0
			value = value - self.time_shield_absorb
		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)
dg's avatar
dg committed
		end
	end
dg's avatar
dg committed
	if self:attr("damage_shield") then
		-- Absorb damage into the shield
		if value <= self.damage_shield_absorb then
			self.damage_shield_absorb = self.damage_shield_absorb - value
			value = 0
		else
			self.damage_shield_absorb = 0
			value = value - self.damage_shield_absorb
		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 value <= self.displacement_shield and rng.percent(self.displacement_shield_chance) 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)
dg's avatar
dg committed
			self:removeEffect(self.EFF_BONE_SHIELD)
			value = 0
		end
	end
dg's avatar
dg committed

dg's avatar
dg committed
	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

dg's avatar
dg committed
	-- 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

dg's avatar
dg committed
	-- Achievements
dg's avatar
dg committed
	if src and src.resolveSource and src:resolveSource().player and value >= 600 then
dg's avatar
dg committed
		world:gainAchievement("SIZE_MATTERS", src:resolveSource())
	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

dg's avatar
dg committed
	-- Adds hate
	if self:knowTalent(self.T_HATE_POOL) then
		local hateGain = 0
		local hateMessage
dg's avatar
dg committed

dg's avatar
dg committed
		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
dg's avatar
dg committed

dg's avatar
dg committed
		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
dg's avatar
dg committed

dg's avatar
dg committed
		if hateGain >= 0.1 then
			self.hate = math.min(self.max_hate, self.hate + hateGain)
			if hateMessage then
dg's avatar
dg committed
				game.logPlayer(self, hateMessage.." (+%0.1f hate)", hateGain)
dg's avatar
dg committed
			end
		end
	end
	if src and src.knowTalent and src:knowTalent(src.T_HATE_POOL) then
		local hateGain = 0
		local hateMessage
dg's avatar
dg committed

dg's avatar
dg committed
		if value / src.max_life > 0.33 then
			-- you deliver a big hit
			hateGain = hateGain + 0.4
			hateMessage = "#F53CBE#Your powerful attack feeds your madness!"
		end
dg's avatar
dg committed

dg's avatar
dg committed
		if hateGain >= 0.1 then
			src.hate = math.min(src.max_hate, src.hate + hateGain)
			if hateMessage then
dg's avatar
dg committed
				game.logPlayer(src, hateMessage.." (+%0.1f hate)", hateGain)
dg's avatar
dg committed
			end
		end
	end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Bloodlust!
	if src and src.knowTalent and src:knowTalent(src.T_BLOODLUST) then
		src:setEffect(src.EFF_BLOODLUST, 1, {})
	end

dg's avatar
dg committed
	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

	-- 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.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

dg's avatar
dg committed
	return value
end

dg's avatar
dg committed
function _M:resolveSource()
	if self.summoner_gain_exp and self.summoner then
		return self.summoner:resolveSource()
	else
		return self
	end
end

dg's avatar
dg committed
function _M:die(src)
dg's avatar
dg committed
	engine.interface.ActorLife.die(self, src)

dg's avatar
dg committed
	-- Gives the killer some exp for the kill
dg's avatar
dg committed
	if src and src.resolveSource and src:resolveSource().gainExp then
		local killer = src:resolveSource()
		killer:gainExp(self:worthExp(killer))
		-- Hack: even if the boss dies from something else, give the player exp
		if not killer.player and self.rank > 3 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
dg's avatar
dg committed
	end
dg's avatar
dg committed
	-- Do we get a blooooooody death ?
	if rng.percent(33) then self:bloodyDeath() end
dg's avatar
dg committed

	-- Drop stuff
dg's avatar
dg committed
	if not self.keep_inven_on_death then
		if not self.no_drops then
			for inven_id, inven in pairs(self.inven) 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", {unique=true, not_properties={"lore"}}, nil, true)
						if ro then game.zone:addEntity(game.level, ro, "object", self.x, self.y) end
					end

dg's avatar
dg committed
					if not o.no_drop then
						o.droppedBy = self.name
dg's avatar
dg committed
						game.level.map:addObject(self.x, self.y, o)
dg's avatar
dg committed
					end
dg's avatar
dg committed
			end
		end
dg's avatar
dg committed
		self.inven = {}
dg's avatar
dg committed
	-- Give stamina back
dg's avatar
dg committed
	if src and src.knowTalent and src:knowTalent(src.T_UNENDING_FRENZY) then
dg's avatar
dg committed
		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

dg's avatar
dg committed
	-- Adds hate
dg's avatar
dg committed
	if src and src.knowTalent and src:knowTalent(self.T_HATE_POOL) then
dg's avatar
dg committed
		local hateGain = src.hate_per_kill
		local hateMessage
dg's avatar
dg committed

dg's avatar
dg committed
		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
dg's avatar
dg committed

		if self.rank >= 4 then
dg's avatar
dg committed
			-- 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!"
dg's avatar
dg committed
		end
		hateGain = math.min(hateGain, 10)
dg's avatar
dg committed

dg's avatar
dg committed
		src.hate = math.min(src.max_hate, src.hate + hateGain)
		if hateMessage then
dg's avatar
dg committed
			game.logPlayer(src, hateMessage.." (+%0.1f hate)", hateGain - src.hate_per_kill)
dg's avatar
dg committed
		end
	end
dg's avatar
dg committed

dg's avatar
dg committed
	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

dg's avatar
dg committed
	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.hasEffect and src:hasEffect(self.EFF_UNSTOPPABLE) then
		local p = src:hasEffect(self.EFF_UNSTOPPABLE)
		p.kills = p.kills + 1
	end

dg's avatar
dg committed
	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

dg's avatar
dg committed
	-- Increase vim
	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
dg's avatar
dg committed
	if src and src.resolveSource and src:resolveSource().player then
dg's avatar
dg committed
		-- Achievements
dg's avatar
dg committed
		local p = src:resolveSource()
		if math.floor(p.life) <= 1 and not p.dead then world:gainAchievement("THAT_WAS_CLOSE", p) end
dg's avatar
dg committed
		world:gainAchievement("EXTERMINATOR", p, self)
		world:gainAchievement("PEST_CONTROL", p, self)
dg's avatar
dg committed
		world:gainAchievement("REAVER", p, self)
dg's avatar
dg committed

		if self.unique then
			p:registerUniqueKilled(self)
		end

dg's avatar
dg committed
		-- Record kills
dg's avatar
dg committed
		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

dg's avatar
dg committed
	return true
dg's avatar
dg committed
end
dg's avatar
dg committed

function _M:learnStats(statorder)
	self.auto_stat_cnt = self.auto_stat_cnt or 1
dg's avatar
dg committed
	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
dg's avatar
dg committed
			self:incStat(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)
dg's avatar
dg committed
		nb = nb + 1
		if nb >= #statorder then break end
dg's avatar
dg committed
function _M:resetToFull()
	self.life = self.max_life
	self.mana = self.max_mana
dg's avatar
dg committed
	self.vim = self.max_vim
dg's avatar
dg committed
	self.stamina = self.max_stamina
	self.equilibrium = 0
dg's avatar
dg committed
end

dg's avatar
dg committed
function _M:levelup()
dg's avatar
dg committed
	self.unused_stats = self.unused_stats + 3 + self:getRankStatAdjust()
	self.unused_talents = self.unused_talents + 1
	self.unused_generics = self.unused_generics + 1
dg's avatar
dg committed
	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
dg's avatar
dg committed
	-- 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
dg's avatar
dg committed
		self.unused_talents_types = self.unused_talents_types + 1
	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,
			}
			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)
dg's avatar
dg committed
		if (self.resists[t] or 0) < 50 then
			self.resists[t] = (self.resists[t] or 0) + rng.float(self:getRankResistAdjust())
		end
dg's avatar
dg committed

		-- 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
dg's avatar
dg committed
	-- Gain life and resources
dg's avatar
dg committed
	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
dg's avatar
dg committed
	self.max_life = self.max_life + math.max(self:getRankLifeAdjust(rating), 1)
dg's avatar
dg committed

dg's avatar
dg committed
	self:incMaxVim(self.vim_rating)
dg's avatar
dg committed
	self:incMaxMana(self.mana_rating)
	self:incMaxStamina(self.stamina_rating)
dg's avatar
dg committed
	self:incMaxPositive(self.positive_negative_rating)
	self:incMaxNegative(self.positive_negative_rating)
dg's avatar
dg committed
	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
dg's avatar
dg committed
	-- Heal up on new level
	self:resetToFull()
dg's avatar
dg committed

	-- Auto levelup ?
	if self.autolevel then
dg's avatar
dg committed
		engine.Autolevel:autoLevel(self)
dg's avatar
dg committed
	end

	-- Force levelup of the golem