Skip to content
Snippets Groups Projects
Combat.lua 23.2 KiB
Newer Older
dg's avatar
dg committed
-- ToME - Tales of Middle-Earth
-- 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"
dg's avatar
dg committed
local DamageType = require "engine.DamageType"
local Map = require "engine.Map"
local Chat = require "engine.Chat"
dg's avatar
dg committed
local Target = require "engine.Target"
dg's avatar
dg committed
local Talents = require "engine.interface.ActorTalents"
dg's avatar
dg committed

--- Interface to add ToME combat system
module(..., package.seeall, class.make)

--- Checks what to do with the target
-- Talk ? attack ? displace ?
function _M:bumpInto(target)
	local reaction = self:reactionToward(target)
	if reaction < 0 then
		return self:attackTarget(target)
	elseif reaction >= 0 then
		-- Talk ?
		if self.player and target.can_talk then
			local chat = Chat.new(target.can_talk, target, self)
			chat:invoke()
			if target.can_talk_only_once then target.can_talk = nil end
		elseif target.player and self.can_talk then
			local chat = Chat.new(self.can_talk, self, target)
			chat:invoke()
			if target.can_talk_only_once then target.can_talk = nil end
dg's avatar
dg committed
		elseif self.move_others then
			-- Displace
dg's avatar
dg committed
			local tx, ty, sx, sy = target.x, target.y, self.x, self.y
			target.x = nil target.y = nil
			self.x = nil self.y = nil
			target:move(sx, sy, true)
			self:move(tx, ty, true)
		end
	end
end

--- Makes the death happen!
dg's avatar
dg committed
--[[
The ToME combat system has the following attributes:
dg's avatar
dg committed
- attack: increases chances to hit against high defense
- defense: increases chances to miss against high attack power
dg's avatar
dg committed
- armor: direct reduction of damage done
- armor penetration: reduction of target's armor
- damage: raw damage done
]]
function _M:attackTarget(target, damtype, mult, noenergy)
dg's avatar
dg committed
	local speed, hit = nil, false
dg's avatar
dg committed
	local sound, sound_miss = nil, nil
dg's avatar
dg committed

dg's avatar
dg committed
	if self:attr("feared") then
		if not noenergy then
			self:useEnergy(game.energy_to_act * speed)
			self.did_energy = true
		end
		game.logSeen(self, "%s is too afraid to attack.", self.name:capitalize())
		return false
	end

dg's avatar
dg committed
	-- Cancel stealth early if we are noticed
	if self:isTalentActive(self.T_STEALTH) and target:canSee(self) then
		self:useTalent(self.T_STEALTH)
		self.changed = true
		game.logPlayer(self, "%s notices you at the last moment!", target.name:capitalize())
	end

dg's avatar
dg committed
	if not self:attr("disarmed") then
		-- All weapons in main hands
		if self:getInven(self.INVEN_MAINHAND) then
			for i, o in ipairs(self:getInven(self.INVEN_MAINHAND)) do
				if o.combat then
					print("[ATTACK] attacking with", o.name)
					local s, h = self:attackTargetWith(target, o.combat, damtype, mult)
					speed = math.max(speed or 0, s)
					hit = hit or h
					if hit and not sound then sound = o.combat.sound
					elseif not hit and not sound_miss then sound_miss = o.combat.sound_miss end
				end
dg's avatar
dg committed
			end
		end
dg's avatar
dg committed
		-- All wpeaons in off hands
		-- Offhand atatcks are with a damage penality, taht can be reduced by talents
		if self:getInven(self.INVEN_OFFHAND) then
			local offmult = (mult or 1) / 2
			if self:knowTalent(Talents.T_DUAL_WEAPON_TRAINING) then
				offmult = (mult or 1) / (2 - (self:getTalentLevel(Talents.T_DUAL_WEAPON_TRAINING) / 6))
dg's avatar
dg committed
			elseif self:knowTalent(Talents.T_CORRUPTED_STRENGTH) then
				offmult = (mult or 1) / (2 - (self:getTalentLevel(Talents.T_CORRUPTED_STRENGTH) / 9))
dg's avatar
dg committed
			end
			for i, o in ipairs(self:getInven(self.INVEN_OFFHAND)) do
				if o.combat then
					print("[ATTACK] attacking with", o.name)
					local s, h = self:attackTargetWith(target, o.combat, damtype, offmult)
					speed = math.max(speed or 0, s)
					hit = hit or h
					if hit and not sound then sound = o.combat.sound
					elseif not hit and not sound_miss then sound_miss = o.combat.sound_miss end
				end
dg's avatar
dg committed
			end
		end
	end

	-- Barehanded ?
dg's avatar
dg committed
	if not speed and self.combat then
dg's avatar
dg committed
		print("[ATTACK] attacking with inate combat")
dg's avatar
dg committed
		local s, h = self:attackTargetWith(target, self.combat, damtype, mult)
		speed = math.max(speed or 0, s)
		hit = hit or h
dg's avatar
dg committed
		if hit and not sound then sound = self.combat.sound
		elseif not hit and not sound_miss then sound_miss = self.combat.sound_miss end
dg's avatar
dg committed
	end

dg's avatar
dg committed
	-- Mount attack ?
	local mount = self:hasMount()
	if mount and mount.mount.attack_with_rider and math.floor(core.fov.distance(self.x, self.y, target.x, target.y)) <= 1 then
		mount.mount.actor:attackTarget(target, nil, nil, nil)
	end

dg's avatar
dg committed
	-- We use up our own energy
	if speed and not noenergy then
dg's avatar
dg committed
		self:useEnergy(game.energy_to_act * speed)
		self.did_energy = true
	end
dg's avatar
dg committed

dg's avatar
dg committed
	if sound then game:playSoundNear(self, sound)
	elseif sound_miss then game:playSoundNear(self, sound_miss) end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Cancel stealth!
dg's avatar
dg committed
	self:breakStealth()
dg's avatar
dg committed
	return hit
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Computes a logarithmic chance to hit, opposing chance to hit to chance to miss
-- This will be used for melee attacks, physical and spell resistance
function _M:checkHit(atk, def, min, max, factor)
dg's avatar
dg committed
	print("checkHit", atk, def)
dg's avatar
dg committed
	if atk == 0 then atk = 1 end
	local hit = nil
	factor = factor or 5
	if atk > def then
dg's avatar
dg committed
		local d = atk - def
		hit = math.log10(1 + 5 * d / 50) * 100 + 50
dg's avatar
dg committed
	else
dg's avatar
dg committed
		local d = def - atk
		hit = -math.log10(1 + 5 * d / 50) * 100 + 50
dg's avatar
dg committed
	end
	hit = util.bound(hit, min or 5, max or 95)
dg's avatar
dg committed
	print("=> chance to hit", hit)
	return rng.percent(hit), hit
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Try to totaly evade an attack
function _M:checkEvasion(target)
	if not target:attr("evasion") then return end

	local evasion = target:attr("evasion")
	print("checkEvasion", evasion, target.level, self.level)
	evasion = evasion * (target.level / self.level)
	print("=> evasion chance", evasion)
	return rng.percent(evasion)
end

dg's avatar
dg committed
--- Attacks with one weapon
dg's avatar
dg committed
function _M:attackTargetWith(target, weapon, damtype, mult)
	damtype = damtype or weapon.damtype or DamageType.PHYSICAL
dg's avatar
dg committed
	mult = mult or 1

dg's avatar
dg committed
	-- Does the blow connect? yes .. complex :/
	local atk, def = self:combatAttack(weapon), target:combatDefense()
dg's avatar
dg committed
	if not self:canSee(target) then atk = atk / 3 end
dg's avatar
dg committed
	local dam, apr, armor = self:combatDamage(weapon), self:combatAPR(weapon), target:combatArmor()
dg's avatar
dg committed
	print("[ATTACK] to ", target.name, " :: ", dam, apr, armor, "::", mult)
dg's avatar
dg committed

	-- If hit is over 0 it connects, if it is 0 we still have 50% chance
dg's avatar
dg committed
	local hitted = false
dg's avatar
dg committed
	local evaded = false
	if self:checkEvasion(target) then
		evaded = true
		game.logSeen(target, "%s evades %s.", target.name:capitalize(), self.name)
	elseif self:checkHit(atk, def) then
dg's avatar
dg committed
		print("[ATTACK] raw dam", dam, "versus", armor, "with APR", apr)
		dam = math.max(0, dam - math.max(0, armor - apr))
dg's avatar
dg committed
		local damrange = self:combatDamageRange(weapon)
		dam = rng.range(dam, dam * damrange)
dg's avatar
dg committed
		print("[ATTACK] after range", dam)
dg's avatar
dg committed
		local crit
dg's avatar
dg committed
		dam, crit = self:physicalCrit(dam, weapon, target)
dg's avatar
dg committed
		print("[ATTACK] after crit", dam)
dg's avatar
dg committed
		dam = dam * mult
		print("[ATTACK] after mult", dam)
dg's avatar
dg committed
		if crit then game.logSeen(self, "%s performs a critical stike!", self.name:capitalize()) end
dg's avatar
dg committed
		DamageType:get(damtype).projector(self, target.x, target.y, damtype, math.max(0, dam))
dg's avatar
dg committed
		hitted = true
dg's avatar
dg committed
	else
		local srcname = game.level.map.seens(self.x, self.y) and self.name:capitalize() or "Something"
		game.logSeen(target, "%s misses %s.", srcname, target.name)
dg's avatar
dg committed
	end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Spread diseases
	if hitted and self:knowTalent(self.T_CARRIER) and rng.percent(4 * self:getTalentLevelRaw(self.T_CARRIER)) then
		-- Use epidemic talent spreading
		local t = self:getTalentFromId(self.T_EPIDEMIC)
		t.do_spread(self, t, target)
	end

dg's avatar
dg committed
	-- Melee project
	if hitted and not target.dead then for typ, dam in pairs(self.melee_project) do
dg's avatar
dg committed
		if dam > 0 then
			DamageType:get(typ).projector(self, target.x, target.y, typ, dam)
		end
dg's avatar
dg committed
	end end

dg's avatar
dg committed
	-- Weapon of light cast
	if hitted and not target.dead and self:knowTalent(self.T_WEAPON_OF_LIGHT) and self:isTalentActive(self.T_WEAPON_OF_LIGHT) then
dg's avatar
dg committed
		local dam = 7 + self:getTalentLevel(self.T_WEAPON_OF_LIGHT) * self:combatSpellpower(0.092)
		DamageType:get(DamageType.LIGHT).projector(self, target.x, target.y, DamageType.LIGHT, dam)
		self:incPositive(-3)
		if self:getPositive() <= 0 then
			local old = self.energy.value
			self.energy.value = 100000
			self:useTalent(self.T_WEAPON_OF_LIGHT)
			self.energy.value = old
		end
	end

dg's avatar
dg committed
	-- Shadow cast
	if hitted and not target.dead and self:knowTalent(self.T_SHADOW_COMBAT) and self:isTalentActive(self.T_SHADOW_COMBAT) and self:getMana() > 0 then
dg's avatar
dg committed
		local dam = 3 + self:getTalentLevel(self.T_SHADOW_COMBAT) * 2
		local mana = 1 + self:getTalentLevelRaw(t) / 1.5
		DamageType:get(DamageType.DARKNESS).projector(self, target.x, target.y, DamageType.DARKNESS, dam)
		self:incMana(-mana)
	end

dg's avatar
dg committed
	-- Autospell cast
	if hitted and not target.dead and self:knowTalent(self.T_ARCANE_COMBAT) and self:isTalentActive(self.T_ARCANE_COMBAT) and rng.percent(20 + self:getTalentLevel(self.T_ARCANE_COMBAT) * (1 + self:getDex(9, true))) then
dg's avatar
dg committed
		local spells = {}
		if self:knowTalent(self.T_FLAME) then spells[#spells+1] = self.T_FLAME end
		if self:knowTalent(self.T_FLAMESHOCK) then spells[#spells+1] = self.T_FLAMESHOCK end
		if self:knowTalent(self.T_LIGHTNING) then spells[#spells+1] = self.T_LIGHTNING end
		if self:knowTalent(self.T_CHAIN_LIGHTNING) then spells[#spells+1] = self.T_CHAIN_LIGHTNING end
		local tid = rng.table(spells)
		if tid then
			print("[ARCANE COMBAT] autocast ",self:getTalentFromId(tid).name)
			local old_cd = self:isTalentCoolingDown(self:getTalentFromId(tid))
			local old = self.energy.value
			self.energy.value = 100000
			self:useTalent(tid, nil, nil, nil, target)
dg's avatar
dg committed
			self.energy.value = old
			-- Do not setup a cooldown
			if not old_cd then
				self.talents_cd[tid] = nil
			end
			self.changed = true
		end
	end

	-- On hit talent
	if hitted and not target.dead and weapon.talent_on_hit and next(weapon.talent_on_hit) then
		for tid, data in pairs(weapon.talent_on_hit) do
			if rng.percent(data.chance) then
				local old = self.energy.value
				self.energy.value = 100000
				self:useTalent(tid, nil, data.level, true, target)
				self.energy.value = old
			end
		end
	end

	-- Shattering Impact
	if hitted and self:attr("shattering_impact") then
		local dam = dam * self.shattering_impact
		self:project({type="ball", radius=1}, target.x, target.y, DamageType.PHYSICAL, dam)
		self:incStamina(-15)
	end

	-- Onslaught
	if hitted and self:attr("onslaught") then
		local dir = util.getDir(target.x, target.y, self.x, self.y)
		local lx, ly = util.coordAddDir(self.x, self.y, dir_sides[dir].left)
		local rx, ry = util.coordAddDir(self.x, self.y, dir_sides[dir].right)
		local lt, rt = game.level.map(lx, ly, Map.ACTOR), game.level.map(rx, ry, Map.ACTOR)

		if target:checkHit(self:combatAttack(weapon), target:combatPhysicalResist(), 0, 95, 10) and target:canBe("knockback") then
			target:knockback(self.x, self.y, self:attr("onslaught"))
		end
dg's avatar
dg committed
		if lt and lt:checkHit(self:combatAttack(weapon), lt:combatPhysicalResist(), 0, 95, 10) and lt:canBe("knockback") then
			lt:knockback(self.x, self.y, self:attr("onslaught"))
		end
dg's avatar
dg committed
		if rt and rt:checkHit(self:combatAttack(weapon), rt:combatPhysicalResist(), 0, 95, 10) and r+t:canBe("knockback") then
			rt:knockback(self.x, self.y, self:attr("onslaught"))
		end
	end
dg's avatar
dg committed
	-- Reactive target on hit damage
	if hitted then for typ, dam in pairs(target.on_melee_hit) do
dg's avatar
dg committed
		if dam > 0 then
			DamageType:get(typ).projector(target, self.x, self.y, typ, dam)
		end
dg's avatar
dg committed
	end end

dg's avatar
dg committed
	-- Acid splash
	if hitted and target:knowTalent(target.T_ACID_BLOOD) then
		local t = target:getTalentFromId(target.T_ACID_BLOOD)
		t.do_splash(target, t, self)
	end

dg's avatar
dg committed
	-- Regen on being hit
	if hitted and not target.dead and target:attr("stamina_regen_on_hit") then target:incStamina(target.stamina_regen_on_hit) end
	if hitted and not target.dead and target:attr("mana_regen_on_hit") then target:incMana(target.mana_regen_on_hit) end
	if hitted and not target.dead and target:attr("equilibrium_regen_on_hit") then target:incEquilibrium(-target.equilibrium_regen_on_hit) end

	-- Riposte!
dg's avatar
dg committed
	if not hitted and not target.dead and not evaded and target:knowTalent(target.T_RIPOSTE) and rng.percent(util.bound(target:getTalentLevel(target.T_RIPOSTE) * target:getDex(40), 10, 60)) then
		game.logSeen(self, "%s ripostes!", target.name:capitalize())
		target:attackTarget(self, nil, nil, true)
	end

dg's avatar
dg committed
	return self:combatSpeed(weapon), hitted
dg's avatar
dg committed
end

dg's avatar
dg committed
local weapon_talents = {
	sword = Talents.T_SWORD_MASTERY,
	axe =   Talents.T_AXE_MASTERY,
	mace =  Talents.T_MACE_MASTERY,
	knife = Talents.T_KNIFE_MASTERY,
	whip  = Talents.T_EXOTIC_WEAPONS_MASTERY,
	trident=Talents.T_EXOTIC_WEAPONS_MASTERY,
dg's avatar
dg committed
	bow =   Talents.T_BOW_MASTERY,
dg's avatar
dg committed
	sling = Talents.T_SLING_MASTERY,
dg's avatar
dg committed
	staff = Talents.T_STAFF_MASTERY,
dg's avatar
dg committed
}

--- Checks weapon training
function _M:combatCheckTraining(weapon)
	if not weapon.talented then return 0 end
	if not weapon_talents[weapon.talented] then return 0 end
	return self:getTalentLevel(weapon_talents[weapon.talented])
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the defense
function _M:combatDefense()
dg's avatar
dg committed
	local add = 0
	if self:hasDualWeapon() and self:knowTalent(self.T_DUAL_WEAPON_DEFENSE) then
		add = add + 4 + (self:getTalentLevel(self.T_DUAL_WEAPON_DEFENSE) * self:getDex()) / 12
	end
	return self.combat_def + (self:getDex() - 10) * 0.35 + add + (self:getLck() - 50) * 0.4
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the defense ranged
function _M:combatDefenseRanged()
	return self:combatDefense() + (self.combat_def_ranged or 0)
end

dg's avatar
dg committed
--- Gets the armor
function _M:combatArmor()
dg's avatar
dg committed
	local add = 0
	if self:hasHeavyArmor() and self:knowTalent(self.T_HEAVY_ARMOUR_TRAINING) then
		add = add + self:getTalentLevel(self.T_HEAVY_ARMOUR_TRAINING) * 1.4
dg's avatar
dg committed
	end
	if self:hasMassiveArmor() and self:knowTalent(self.T_MASSIVE_ARMOUR_TRAINING) then
		add = add + self:getTalentLevel(self.T_MASSIVE_ARMOUR_TRAINING) * 1.6
dg's avatar
dg committed
	end
	return self.combat_armor + add
dg's avatar
dg committed
end

--- Gets the attack
function _M:combatAttack(weapon)
	weapon = weapon or self.combat
	return self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 5 + (weapon.atk or 0) + (self:getStr(50) - 5) + (self:getDex(50) - 5) + (self:getLck() - 50) * 0.4
dg's avatar
dg committed
end

--- Gets the attack using only strength
function _M:combatAttackStr(weapon)
	weapon = weapon or self.combat
	return self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 5 + (weapon.atk or 0) + (self:getStr(100) - 10) + (self:getLck() - 50) * 0.4
dg's avatar
dg committed
end

--- Gets the attack using only dexterity
function _M:combatAttackDex(weapon)
	weapon = weapon or self.combat
	return self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 5 + (weapon.atk or 0) + (self:getDex(100) - 10) + (self:getLck() - 50) * 0.4
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the attack using only magic
function _M:combatAttackDex(weapon)
	weapon = weapon or self.combat
	return self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 5 + (weapon.atk or 0) + (self:getMag(100) - 10) + (self:getLck() - 50) * 0.4
end

dg's avatar
dg committed
--- Gets the armor penetration
function _M:combatAPR(weapon)
	weapon = weapon or self.combat
dg's avatar
dg committed
	return self.combat_apr + (weapon.apr or 0)
dg's avatar
dg committed
end

--- Gets the weapon speed
function _M:combatSpeed(weapon)
	weapon = weapon or self.combat
	return self.combat_physspeed + (weapon.physspeed or 1)
end

--- Gets the crit rate
function _M:combatCrit(weapon)
	weapon = weapon or self.combat
dg's avatar
dg committed
	local addcrit = 0
	if weapon.talented and weapon.talented == "knife" and self:knowTalent(Talents.T_LETHALITY) then
		addcrit = 1 + self:getTalentLevel(Talents.T_LETHALITY) * 1.3
	end
	return self.combat_physcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + (weapon.physcrit or 1) + addcrit
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the damage range
function _M:combatDamageRange(weapon)
	weapon = weapon or self.combat
dg's avatar
dg committed
	return (self.combat_damrange or 0) + (weapon.damrange or 1.1)
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the damage
function _M:combatDamage(weapon)
	weapon = weapon or self.combat
dg's avatar
dg committed

	local sub_con_to_str = false
	if weapon.talented and weapon.talented == "knife" and self:knowTalent(Talents.T_LETHALITY) then sub_con_to_str = true end

	local dammod = weapon.dammod or {str=0.6}
	for stat, mod in pairs(dammod) do
		if sub_con_to_str and stat == "str" then stat = "cun" end
		totstat = totstat + self:getStat(stat) * mod
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self:knowTalent(Talents.T_ARCANE_DESTRUCTION) then
		add = add + self:combatSpellpower() * self:getTalentLevel(Talents.T_ARCANE_DESTRUCTION) / 9
	end
	if self:isTalentActive(Talents.T_BLOOD_FRENZY) then
		add = add + self.blood_frenzy
	end
	local talented_mod = math.sqrt(self:combatCheckTraining(weapon) / 10) + 1
dg's avatar
dg committed
	local power = math.max(self.combat_dam + (weapon.dam or 1) + add, 1)
	power = (math.sqrt(power / 10) - 1) * 0.8 + 1
	print(("[COMBAT DAMAGE] power(%f) totstat(%f) talent_mod(%f)"):format(power, totstat, talented_mod))
	return totstat / 1.5 * power * talented_mod
dg's avatar
dg committed
end

--- Gets spellpower
function _M:combatSpellpower(mod)
	mod = mod or 1
dg's avatar
dg committed
	local add = 0
	if self:knowTalent(self.T_ARCANE_DEXTERITY) then
dg's avatar
dg committed
		add = add + (15 + self:getTalentLevel(self.T_ARCANE_DEXTERITY) * 5) * self:getDex() / 100
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_SHADOW_CUNNING) then
dg's avatar
dg committed
		add = add + (15 + self:getTalentLevel(self.T_SHADOW_CUNNING) * 3) * self:getCun() / 100
	end
	if self:hasEffect(self.EFF_BLOODLUST) then
		add = add + self:hasEffect(self.EFF_BLOODLUST).dur
dg's avatar
dg committed
	end
	return (self.combat_spellpower + add + self:getMag()) * mod
end

--- Gets damage based on talent
dg's avatar
dg committed
function _M:combatTalentSpellDamage(t, base, max, spellpower_override)
	-- Compute at "max"
	local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
	-- Compute real
dg's avatar
dg committed
	return (base + (spellpower_override or self:combatSpellpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets weapon damage mult based on talent
function _M:combatTalentWeaponDamage(t, base, max, t2)
	if t2 then t2 = t2 / 2 else t2 = 0 end
	local diff = max - base
dg's avatar
dg committed
	local mult = base + diff * math.sqrt((self:getTalentLevel(t) + t2) / 5)
	print("[TALENT WEAPON MULT]", self:getTalentLevel(t), base, max, t2, mult)
	return mult
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets spellcrit
function _M:combatSpellCrit()
	return self.combat_spellcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + 1
dg's avatar
dg committed
end

--- Gets spellspeed
function _M:combatSpellSpeed()
dg's avatar
dg committed
	return self.combat_spellspeed + 1
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Computes physical crit for a damage
dg's avatar
dg committed
function _M:physicalCrit(dam, weapon, target)
dg's avatar
dg committed
	if self:isTalentActive(self.T_STEALTH) and self:knowTalent(self.T_SHADOWSTRIKE) then
dg's avatar
dg committed
		return dam * (1.5 + self:getTalentLevel(self.T_SHADOWSTRIKE) / 7), true
dg's avatar
dg committed
	end

dg's avatar
dg committed
	local chance = self:combatCrit(weapon)
dg's avatar
dg committed
	local crit = false
dg's avatar
dg committed
	if self:knowTalent(self.T_BACKSTAB) and target:attr("stunned") then chance = chance + self:getTalentLevel(self.T_BACKSTAB) * 10 end
	if target:hasHeavyArmor() and target:knowTalent(target.T_HEAVY_ARMOUR_TRAINING) then
		chance = chance - target:getTalentLevel(target.T_HEAVY_ARMOUR_TRAINING) * 1.9
	end
	if target:hasMassiveArmor() and target:knowTalent(target.T_MASSIVE_ARMOUR_TRAINING) then
		chance = chance - target:getTalentLevel(target.T_MASSIVE_ARMOUR_TRAINING) * 1.5
	end

dg's avatar
dg committed
	chance = util.bound(chance, 0, 100)

dg's avatar
dg committed
	print("[PHYS CRIT %]", chance)
dg's avatar
dg committed
	if rng.percent(chance) then
dg's avatar
dg committed
		dam = dam * 1.5
dg's avatar
dg committed
		crit = true
dg's avatar
dg committed
	end
dg's avatar
dg committed
	return dam, crit
dg's avatar
dg committed
end

--- Computes spell crit for a damage
function _M:spellCrit(dam, add_chance)
dg's avatar
dg committed
	if self:isTalentActive(self.T_STEALTH) and self:knowTalent(self.T_SHADOWSTRIKE) then
dg's avatar
dg committed
		return dam * (1.5 + self:getTalentLevel(self.T_SHADOWSTRIKE) / 7), true
dg's avatar
dg committed
	end

	local chance = self:combatSpellCrit() + (add_chance or 0)
dg's avatar
dg committed
	local crit = false
dg's avatar
dg committed
	print("[SPELL CRIT %]", chance)
dg's avatar
dg committed
	if rng.percent(chance) then
dg's avatar
dg committed
		dam = dam * 1.5
dg's avatar
dg committed
		crit = true
dg's avatar
dg committed
	end
dg's avatar
dg committed
	return dam, crit
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Do we get hit by our own AOE ?
function _M:spellFriendlyFire()
dg's avatar
dg committed
	print("[SPELL] friendly fire chance", self:getTalentLevelRaw(self.T_SPELL_SHAPING) * 20 + (self:getLck() - 50) * 0.2)
dg's avatar
dg committed
	return not rng.percent(self:getTalentLevelRaw(self.T_SPELL_SHAPING) * 20 + (self:getLck() - 50) * 0.2)
--- Gets mindpower
function _M:combatMindpower(mod)
	mod = mod or 1
	local add = 0
	return (self.combat_mindpower + add + self:getWil() * 0.7 + self:getCun() * 0.4) * mod
end

--- Gets damage based on talent
function _M:combatTalentMindDamage(t, base, max)
	-- Compute at "max"
	local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
	-- Compute real
	return (base + (self:combatMindpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod
end

dg's avatar
dg committed
--- Computes physical resistance
function _M:combatPhysicalResist()
	return self.combat_physresist + (self:getCon() + self:getStr() + (self:getLck() - 50) * 0.5) * 0.25
dg's avatar
dg committed
end

--- Computes spell resistance
function _M:combatSpellResist()
	return self.combat_spellresist + (self:getMag() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.25
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Computes mental resistance
function _M:combatMentalResist()
	return self.combat_mentalresist + (self:getCun() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.25
dg's avatar
dg committed
--- Check if the actor has a gem bomb in quiver
function _M:hasAlchemistWeapon()
	if not self:getInven("QUIVER") then return nil, "no ammo" end
	local ammo = self:getInven("QUIVER")[1]
	if not ammo or not ammo.alchemist_power then
		return nil, "bad or no ammo"
	end
	return ammo
end

dg's avatar
dg committed
--- Check if the actor has a staff weapon
function _M:hasStaffWeapon()
dg's avatar
dg committed
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

dg's avatar
dg committed
	if not self:getInven("MAINHAND") then return end
	local weapon = self:getInven("MAINHAND")[1]
	if not weapon or weapon.subtype ~= "staff" then
		return nil
	end
	return weapon
end

--- Check if the actor has a two handed weapon
dg's avatar
dg committed
function _M:hasTwoHandedWeapon()
dg's avatar
dg committed
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

dg's avatar
dg committed
	if not self:getInven("MAINHAND") then return end
dg's avatar
dg committed
	local weapon = self:getInven("MAINHAND")[1]
	if not weapon or not weapon.twohanded then
		return nil
	end
	return weapon
end

--- Check if the actor has a shield
function _M:hasShield()
dg's avatar
dg committed
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

dg's avatar
dg committed
	if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return end
	local shield = self:getInven("OFFHAND")[1]
	if not shield or not shield.special_combat then
		return nil
	end
	return shield
dg's avatar
dg committed
end

--- Check if the actor dual wields
function _M:hasDualWeapon()
dg's avatar
dg committed
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

dg's avatar
dg committed
	if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return end
	local weapon = self:getInven("MAINHAND")[1]
	local offweapon = self:getInven("OFFHAND")[1]
	if not weapon or not offweapon or not weapon.combat or not offweapon.combat then
		return nil
	end
	return weapon, offweapon
end
dg's avatar
dg committed

--- Check if the actor has a heavy armor
function _M:hasHeavyArmor()
	if not self:getInven("BODY") then return end
	local armor = self:getInven("BODY")[1]
	if not armor or armor.subtype ~= "heavy" then
		return nil
	end
dg's avatar
dg committed
	return armor
dg's avatar
dg committed
end

--- Check if the actor has a massive armor
function _M:hasMassiveArmor()
	if not self:getInven("BODY") then return end
	local armor = self:getInven("BODY")[1]
	if not armor or armor.subtype ~= "massive" then
		return nil
	end
dg's avatar
dg committed
	return armor
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Check if the actor has a mount
function _M:hasMount()
	if not self:getInven("MOUNT") then return end
	local mount = self:getInven("MOUNT")[1]
	if not mount or mount.type ~= "mount" then
		return nil
	end
	return mount
end