Skip to content
Snippets Groups Projects
Combat.lua 81.9 KiB
Newer Older
dg's avatar
dg committed
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009 - 2014 Nicolas Casalini
dg's avatar
dg committed
--
-- 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, x, y)
	local reaction = self:reactionToward(target)
	if reaction < 0 then
		if target.encounterAttack and self.player then self:onWorldEncounter(target, x, y) return end
		if game.player == self and ((not config.settings.tome.actor_based_movement_mode and game.bump_attack_disabled) or (config.settings.tome.actor_based_movement_mode and self.bump_attack_disabled)) then return end
		return self:useTalent(self.T_ATTACK, nil, nil, nil, target)
	elseif reaction >= 0 then
dg's avatar
dg committed
		-- Talk ? Bump ?
		if self.player and target.on_bump then
			target:on_bump(self)
		elseif self.player and target.can_talk then
dg's avatar
dg committed
			local chat = Chat.new(target.can_talk, target, self, {npc=target, player=self})
			chat:invoke()
			if target.can_talk_only_once then target.can_talk = nil end
		elseif target.player and self.can_talk then
dg's avatar
dg committed
			local chat = Chat.new(self.can_talk, self, target, {npc=self, player=target})
			chat:invoke()
			if target.can_talk_only_once then target.can_talk = nil end
		elseif self.move_others and not target.cant_be_moved then
			if target.move_others and self ~= game.player then return end

			-- Check we can both walk in the tile we will end up in
			local blocks = game.level.map:checkAllEntitiesLayersNoStop(target.x, target.y, "block_move", self)
			for kind, v in pairs(blocks) do if kind[1] ~= Map.ACTOR and v then return end end
			blocks = game.level.map:checkAllEntitiesLayersNoStop(self.x, self.y, "block_move", target)
			for kind, v in pairs(blocks) do if kind[1] ~= Map.ACTOR and v then return end end

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

dg's avatar
dg committed
			target:move(sx, sy, true)
			self:move(tx, ty, true)
			if target.describeFloor then target:describeFloor(target.x, target.y, true) end
			if self.describeFloor then self:describeFloor(self.x, self.y, true) end
dg's avatar
dg committed

			if self:attr("bump_swap_speed_divide") then
				self:useEnergy(game.energy_to_act * self:combatMovementSpeed(x, y) / self:attr("bump_swap_speed_divide"))
				self.did_energy = true
			end
		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
]]
dg's avatar
dg committed
function _M:attackTarget(target, damtype, mult, noenergy, force_unharmed)
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

	-- Break before we do the blow, because it might start step up, we dont want to insta-cancel it
	self:breakStepUp()
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
	if self:attr("terrified") and rng.percent(self:attr("terrified")) then
		if not noenergy then
			self:useEnergy(game.energy_to_act)
			self.did_energy = true
		end
		game.logSeen(self, "%s is too terrified to attack.", self.name:capitalize())
		return false
	end
dg's avatar
dg committed

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
DarkGod's avatar
DarkGod committed
		if self.player then self:logCombat(target, "#Target# notices you at the last moment!") end
dg's avatar
dg committed
	end

	if target:isTalentActive(target.T_INTUITIVE_SHOTS) and rng.percent(target:callTalent(target.T_INTUITIVE_SHOTS, "getChance")) then
		local ret = target:callTalent(target.T_INTUITIVE_SHOTS, "proc", self)
		if ret then return false end
	end

dg's avatar
dg committed
	-- Change attack type if using gems
	if not damtype and self:getInven(self.INVEN_GEM) then
		local gems = self:getInven(self.INVEN_GEM)
		local types = {}
		for i = 1, #gems do
			if gems[i] and gems[i].attack_type then types[#types+1] = gems[i].attack_type end
		end
		if #types > 0 then
			damtype = rng.table(types)
		end
dg's avatar
dg committed
	elseif not damtype and self:attr("force_melee_damage_type") then
		damtype = self:attr("force_melee_damage_type")
dg's avatar
dg committed
	end

dg's avatar
dg committed
	local break_stealth = false
dg's avatar
dg committed

	local hd = {"Combat:attackTarget", target=target, damtype=damtype, mult=mult, noenergy=noenergy}
	if self:triggerHook(hd) then
		speed, hit, damtype, mult = hd.speed, hd.hit, hd.damtype, hd.mult
dg's avatar
dg committed
		if hd.stop then return hit end
dg's avatar
dg committed
	end

	if not speed and self:isTalentActive(self.T_GESTURE_OF_PAIN) then
		print("[ATTACK] attacking with Gesture of Pain")
		local t = self:getTalentFromId(self.T_GESTURE_OF_PAIN)
dg's avatar
dg committed
		if not t.preAttack(self, t, target) then return false end
		speed, hit = t.attack(self, t, target)
		break_stealth = true
	end

dg's avatar
dg committed
	local mean
	if not speed and not self:attr("disarmed") and not self:isUnarmed() and not force_unharmed then
dg's avatar
dg committed
		-- All weapons in main hands
		if self:getInven(self.INVEN_MAINHAND) then
			for i, o in ipairs(self:getInven(self.INVEN_MAINHAND)) do
dg's avatar
dg committed
				local combat = self:getObjectCombat(o, "mainhand")
				if combat and not o.archery then
dg's avatar
dg committed
					print("[ATTACK] attacking with", o.name)
dg's avatar
dg committed
					local s, h = self:attackTargetWith(target, combat, damtype, mult)
dg's avatar
dg committed
					speed = math.max(speed or 0, s)
					hit = hit or h
dg's avatar
dg committed
					if hit and not sound then sound = combat.sound
					elseif not hit and not sound_miss then sound_miss = combat.sound_miss end
					if not combat.no_stealth_break then break_stealth = true end
dg's avatar
dg committed
				end
dg's avatar
dg committed
			end
		end
dg's avatar
dg committed
		-- All weapons in off hands
		-- Offhand attacks are with a damage penalty, that can be reduced by talents
dg's avatar
dg committed
		if self:getInven(self.INVEN_OFFHAND) then
			for i, o in ipairs(self:getInven(self.INVEN_OFFHAND)) do
dg's avatar
dg committed
				local offmult = self:getOffHandMult(o.combat, mult)
dg's avatar
dg committed
				local combat = self:getObjectCombat(o, "offhand")
dg's avatar
dg committed
				if o.special_combat and o.subtype == "shield" and self:knowTalent(self.T_STONESHIELD) then combat = o.special_combat end
				if combat and not o.archery then
dg's avatar
dg committed
					print("[ATTACK] attacking with", o.name)
dg's avatar
dg committed
					local s, h = self:attackTargetWith(target, combat, damtype, offmult)
dg's avatar
dg committed
					speed = math.max(speed or 0, s)
					hit = hit or h
dg's avatar
dg committed
					if hit and not sound then sound = combat.sound
					elseif not hit and not sound_miss then sound_miss = combat.sound_miss end
					if not combat.no_stealth_break then break_stealth = true end
dg's avatar
dg committed
				end
dg's avatar
dg committed
			end
		end
dg's avatar
dg committed
		mean = "weapon"
dg's avatar
dg committed
	end

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

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

	game:playSoundNear(self, self.on_hit_sound or "actions/melee_hit_squish")
	if self.sound_moam and rng.chance(7) then game:playSoundNear(self, self.sound_moam) end

dg's avatar
dg committed
	-- cleave second attack
	if self:isTalentActive(self.T_CLEAVE) then
dg's avatar
dg committed
		local t = self:getTalentFromId(self.T_CLEAVE)
		t.on_attackTarget(self, t, target)
dg's avatar
dg committed
	if self:attr("unharmed_attack_on_hit") then
		local v = self:attr("unharmed_attack_on_hit")
		self:attr("unharmed_attack_on_hit", -v)
		if rng.percent(60) then self:attackTarget(target, nil, 1, true, true) end
dg's avatar
dg committed
		self:attr("unharmed_attack_on_hit", v)
	end

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

dg's avatar
dg committed
--- Determines the combat field to use for this item
function _M:getObjectCombat(o, kind)
	if kind == "barehand" then return self.combat end
	if kind == "mainhand" then return o.combat end
	if kind == "offhand" then return o.combat end
	return nil
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
dg's avatar
dg committed

function _M:checkHitOld(atk, def, min, max, factor)
	if atk < 0 then atk = 0 end
	if def < 0 then def = 0 end
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

	local one = 1 / (1 + math.exp(-(atk - def) / 7))
	local two = 0
dg's avatar
dg committed
	if atk + def ~= 0 then two = atk / (atk + def) end
dg's avatar
dg committed
	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
--Tells the tier difference between two values
function _M:crossTierEffect(eff_id, apply_power, apply_save, use_given_e)
	local q = game.player:hasQuest("tutorial-combat-stats")
	if q and not q:isCompleted("final-lesson")then
		return
	end
	local ct_effect
	local save_for_effects = {
		physical = "combatPhysicalResist",
		magical = "combatSpellResist",
		mental = "combatMentalResist",
	}
	local cross_tier_effects = {
		combatPhysicalResist = self.EFF_OFFBALANCE,
		combatSpellResist = self.EFF_SPELLSHOCKED,
		combatMentalResist = self.EFF_BRAINLOCKED,
	}
	local e = self.tempeffect_def[eff_id]
	if not apply_power or not save_for_effects[e.type] then return end
dg's avatar
dg committed
	local save = self[apply_save or save_for_effects[e.type]](self, true)
dg's avatar
dg committed

	if use_given_e then
		ct_effect = self["EFF_"..e.name]
	else
		ct_effect = cross_tier_effects[save_for_effects[e.type]]
	end
	local dur = self:getTierDiff(apply_power, save)
dg's avatar
dg committed
	self:setEffect(ct_effect, dur, {})
dg's avatar
dg committed
end

function _M:getTierDiff(atk, def)
	atk = math.floor(atk)
	def = math.floor(def)
	return math.max(0, math.max(math.ceil(atk/20), 1) - math.max(math.ceil(def/20), 1))
end

--New, simpler checkHit that relies on rescaleCombatStats() being used elsewhere
function _M:checkHit(atk, def, min, max, factor, p)
	if atk < 0 then atk = 0 end
	if def < 0 then def = 0 end
dg's avatar
dg committed
	local min = min or 0
dg's avatar
dg committed
	local max = max or 100
dg's avatar
dg committed
	if game.player:hasQuest("tutorial-combat-stats") then
		min = 0
		max = 100
	end --ensures predictable combat for the tutorial
	print("checkHit", atk, def)
	local hit = math.ceil(50 + 2.5 * (atk - def))
dg's avatar
dg committed
	hit = util.bound(hit, min, max)
	print("=> chance to hit", hit)
	return rng.percent(hit), hit
end

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

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

function _M:getAccuracyEffect(weapon, atk, def, scale, max)
	max = max or 10000000
	scale = scale or 1
	return math.min(max, math.max(0, atk - def) * scale * (weapon.accuracy_effect_scale or 1))
end

function _M:isAccuracyEffect(weapon, kind)
DarkGod's avatar
DarkGod committed
	if not weapon then return false, "none" end
	local eff = weapon.accuracy_effect or weapon.talented
	return eff == kind, eff
end

dg's avatar
dg committed
--- Attacks with one weapon
function _M:attackTargetWith(target, weapon, damtype, mult, force_dam)
	damtype = damtype or (weapon and weapon.damtype) or DamageType.PHYSICAL
dg's avatar
dg committed
	mult = mult or 1
	
	--Life Steal
	if weapon and weapon.lifesteal then
		self:attr("lifesteal", weapon.lifesteal)
		self:attr("silent_heal", 1)
	end
dg's avatar
dg committed

	local mode = "other"
	if self:hasShield() then mode = "shield"
	elseif self:hasTwoHandedWeapon() then mode = "twohanded" 
	elseif self:hasDualWeapon() then mode = "dualwield"
	end
dg's avatar
dg committed
	self.turn_procs.weapon_type = {kind=weapon and weapon.talented or "unknown", mode=mode}
dg's avatar
dg committed
	-- Does the blow connect? yes .. complex :/
	local atk, def = self:combatAttack(weapon), target:combatDefense()
dg's avatar
dg committed

	-- add stalker damage and attack bonus
	local effStalker = self:hasEffect(self.EFF_STALKER)
	if effStalker and effStalker.target == target then
		local t = self:getTalentFromId(self.T_STALK)
		atk = atk + t.getAttackChange(self, t, effStalker.bonus)
		mult = mult * t.getStalkedDamageMultiplier(self, t, effStalker.bonus)
	end
	-- add marked prey damage and attack bonus
	local effPredator = self:hasEffect(self.EFF_PREDATOR)
	if effPredator and effPredator.type == target.type then
		if effPredator.subtype == target.subtype then
			mult = mult + effPredator.subtypeDamageChange
			atk = atk + effPredator.subtypeAttackChange
		else
			mult = mult + effPredator.typeDamageChange
			atk = atk + effPredator.typeAttackChange
		end
	end
dg's avatar
dg committed

	-- track weakness for hate bonus before the target removes it
	local effGloomWeakness = target:hasEffect(target.EFF_GLOOM_WEAKNESS)
dg's avatar
dg committed

	local dam, apr, armor = force_dam or self:combatDamage(weapon), self:combatAPR(weapon), target:combatArmor()
	print("[ATTACK] to ", target.name, " :: ", dam, apr, armor, def, "::", mult)

	-- check repel
	local repelled = false
	if target:isTalentActive(target.T_REPEL) then
		local t = target:getTalentFromId(target.T_REPEL)
		repelled = t.isRepelled(target, t)
	end
dg's avatar
dg committed

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
	local crit = false
dg's avatar
dg committed
	local evaded = false
	if repelled then
DarkGod's avatar
DarkGod committed
		self:logCombat(target, "#Target# repels an attack from #Source#.")
	elseif self:checkEvasion(target) then
dg's avatar
dg committed
		evaded = true
DarkGod's avatar
DarkGod committed
		self:logCombat(target, "#Target# evades #Source#.")
dg's avatar
dg committed
	elseif self:checkHit(atk, def) and (self:canSee(target) or self:attr("blind_fight") or rng.chance(3)) then
dg's avatar
dg committed
		local pres = util.bound(target:combatArmorHardiness() / 100, 0, 1)
		if target.knowTalent and target:hasEffect(target.EFF_DUAL_WEAPON_DEFENSE) then
DarkGod's avatar
DarkGod committed
			local deflect = math.min(dam, target:callTalent(target.T_DUAL_WEAPON_DEFENSE, "doDeflect"))
			if deflect > 0 then
DarkGod's avatar
DarkGod committed
--				self:logCombat(target, "#Target# parries %d damage from #Source#'s attack.", deflect)
				game:delayedLogDamage(self, target, 0, ("%s(%d parried#LAST#)"):format(DamageType:get(damtype).text_color or "#aaaaaa#", deflect), false)
				dam = math.max(dam - deflect,0)
				print("[ATTACK] after DUAL_WEAPON_DEFENSE", dam)
			end 
		end
DarkGod's avatar
DarkGod committed
		if target.knowTalent and target:hasEffect(target.EFF_GESTURE_OF_GUARDING) and not target:attr("encased_in_ice") then
			local deflected = math.min(dam, target:callTalent(target.T_GESTURE_OF_GUARDING, "doGuard")) or 0
DarkGod's avatar
DarkGod committed
--			if deflected > 0 then self:logCombat(target, "#Target# dismisses %d damage from #Source#'s attack with a sweeping gesture.", deflected) end
			if deflected > 0 then
				game:delayedLogDamage(self, target, 0, ("%s(%d gestured#LAST#)"):format(DamageType:get(damtype).text_color or "#aaaaaa#", deflected), false)
				dam = dam - deflected
			end
			print("[ATTACK] after GESTURE_OF_GUARDING", dam)
		end

		if self:isAccuracyEffect(weapon, "knife") then
			local bonus = 1 + self:getAccuracyEffect(weapon, atk, def, 0.005, 0.25)
			print("[ATTACJ] dagger accuracy bonus", atk, def, "=", bonus, "previous", apr)
			apr = apr * bonus
		end

		print("[ATTACK] raw dam", dam, "versus", armor, pres, "with APR", apr)
dg's avatar
dg committed
		armor = math.max(0, armor - apr)
		dam = math.max(dam * pres - armor, 0) + (dam * (1 - pres))
dg's avatar
dg committed
		print("[ATTACK] after armor", dam)
dg's avatar
dg committed
		local damrange = self:combatDamageRange(weapon)
		dam = rng.range(dam, dam * damrange)
		print("[ATTACK] after range", dam)
dg's avatar
dg committed
		dam, crit = self:physicalCrit(dam, weapon, target, atk, def)
dg's avatar
dg committed
		print("[ATTACK] after crit", dam)
dg's avatar
dg committed
		dam = dam * mult
		print("[ATTACK] after mult", dam)
		if self:isAccuracyEffect(weapon, "mace") then
			local bonus = 1 + self:getAccuracyEffect(weapon, atk, def, 0.001, 0.1)
			print("[ATTACK] mace accuracy bonus", atk, def, "=", bonus)
			dam = dam * bonus
		end

		if target:hasEffect(target.EFF_COUNTERSTRIKE) then
			dam = dam * 2
			local eff = target.tmp[target.EFF_COUNTERSTRIKE]
			eff.nb = eff.nb - 1
			if eff.nb == 0 then target:removeEffect(target.EFF_COUNTERSTRIKE) end
			print("[ATTACK] after counterstrike", dam)
		end

		if weapon and weapon.inc_damage_type then
			for t, idt in pairs(weapon.inc_damage_type) do
				if target.type.."/"..target.subtype == t or target.type == t then dam = dam + dam * idt / 100 break end
			end
			print("[ATTACK] after inc by type", dam)
		end
dg's avatar
dg committed

DarkGod's avatar
DarkGod committed
		if crit then self:logCombat(target, "#{bold}##Source# performs a melee critical strike against #Target#!#{normal}#") end

		-- Phasing, percent of weapon damage bypasses shields
		-- It's done like this because onTakeHit has no knowledge of the weapon
		if weapon and weapon.phasing then
			self:attr("damage_shield_penetrate", weapon.phasing)
		end
	
		local oldproj = DamageType:getProjectingFor(self)
		if self.__talent_running then DamageType:projectingFor(self, {project_type={talent=self.__talent_running}}) end

		-- Damage conversion?
		-- Reduces base damage but converts it into another damage type
		local conv_dam
		local conv_damtype
		if weapon and weapon.convert_damage then
			for typ, conv in pairs(weapon.convert_damage) do
				if dam > 0 then
					conv_dam = math.min(dam, dam * (conv / 100))
					total_conversion = total_conversion + conv_dam
					conv_damtype = typ
					dam = dam - conv_dam
					if conv_dam > 0 then
						DamageType:get(conv_damtype).projector(self, target.x, target.y, conv_damtype, math.max(0, conv_dam))
					end
				end
			end
		end

		if dam > 0 then
			DamageType:get(damtype).projector(self, target.x, target.y, damtype, math.max(0, dam))
		end
		if self.__talent_running then DamageType:projectingFor(self, oldproj) end

		if weapon and weapon.phasing then
			self:attr("damage_shield_penetrate", -weapon.phasing)
		end

		-- add damage conversion back in so the total damage still gets passed
		if total_conversion > 0 then
			dam = dam + total_conversion
		end
		target:fireTalentCheck("callbackOnMeleeHit", self)
dg's avatar
dg committed

dg's avatar
dg committed
		hitted = true
dg's avatar
dg committed
	else
DarkGod's avatar
DarkGod committed
		self:logCombat(target, "#Source# misses #Target#.")
		target:fireTalentCheck("callbackOnMeleeMiss", self)
dg's avatar
dg committed
	end
dg's avatar
dg committed

	-- cross-tier effect for accuracy vs. defense
	local tier_diff = self:getTierDiff(atk, target:combatDefense(false, target:attr("combat_def_ct")))
	if hitted and not target.dead and tier_diff > 0 then
		local reapplied = false
		-- silence the apply message it if the target already has the effect
		for eff_id, p in pairs(target.tmp) do
			local e = target.tempeffect_def[eff_id]
			if e.desc == "Off-guard" then
				reapplied = true
			end
		end
		target:setEffect(target.EFF_OFFGUARD, tier_diff, {}, reapplied)

	if self:isAccuracyEffect(weapon, "staff") then
		local bonus = 1 + self:getAccuracyEffect(weapon, atk, def, 0.04, 2)
		print("[ATTACK] staff accuracy bonus", atk, def, "=", bonus)
		self.__global_accuracy_damage_bonus = bonus
	end

	-- handle stalk targeting for hits (also handled in Actor for turn end effects)
	if hitted and target ~= self then
		if effStalker then
			-- mark if stalkee was hit
			effStalker.hit = effStalker.hit or effStalker.target == target
		elseif self:isTalentActive(self.T_STALK) then
			local stalk = self:isTalentActive(self.T_STALK)
dg's avatar
dg committed

			if not stalk.hit then
				-- mark a new target
				stalk.hit = true
				stalk.hit_target = target
			elseif stalk.hit_target ~= target then
				-- more than one target; clear it
				stalk.hit_target = nil
			end
		end
	end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Spread diseases
	if hitted and self:knowTalent(self.T_CARRIER) and rng.percent(self:callTalent(self.T_CARRIER, "getDiseaseSpread")) then
dg's avatar
dg committed
		-- Use epidemic talent spreading
		self:callTalent(self.T_EPIDEMIC, "do_spread", target, dam)
dg's avatar
dg committed
	end

dg's avatar
dg committed
	-- Melee project
	if hitted and not target.dead and weapon and weapon.melee_project then for typ, dam in pairs(weapon.melee_project) do
		if dam > 0 then
			DamageType:get(typ).projector(self, target.x, target.y, typ, dam)
		end
	end end
	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
		if self:getPositive() >= 3 then
			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)
dg's avatar
dg committed
		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 = 2 + self:combatTalentSpellDamage(self.T_SHADOW_COMBAT, 2, 50)
		local mana = 2
		if self:getMana() > mana then
			DamageType:get(DamageType.DARKNESS).projector(self, target.x, target.y, DamageType.DARKNESS, dam)
			self:incMana(-mana)
		end
	if hitted and not target.dead and self:knowTalent(self.T_WEAPON_FOLDING) and self:isTalentActive(self.T_WEAPON_FOLDING) then
		local t = self:getTalentFromId(self.T_WEAPON_FOLDING)
		local dam = t.getDamage(self, t)
		DamageType:get(DamageType.TEMPORAL).projector(self, target.x, target.y, DamageType.TEMPORAL, dam)
		self:incParadox(- t.getParadoxReduction(self, t))
	-- Ruin
	if hitted and not target.dead and self:knowTalent(self.T_RUIN) and self:isTalentActive(self.T_RUIN) then
		local t = self:getTalentFromId(self.T_RUIN)
		local dam = t.getDamage(self, t)
		DamageType:get(DamageType.DRAINLIFE).projector(self, target.x, target.y, DamageType.DRAINLIFE, dam)
	end
dg's avatar
dg committed

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) then
		local t = self:getTalentFromId(self.T_ARCANE_COMBAT)
		t.do_trigger(self, t, target)
dg's avatar
dg committed
	end

dg's avatar
dg committed
	-- On hit talent
	if hitted and not target.dead and weapon and weapon.talent_on_hit and next(weapon.talent_on_hit) and not self.turn_procs.melee_talent then
		for tid, data in pairs(weapon.talent_on_hit) do
			if rng.percent(data.chance) then
				self.turn_procs.melee_talent = true
				self:forceUseTalent(tid, {ignore_cd=true, ignore_energy=true, force_target=target, force_level=data.level, ignore_ressources=true})
	-- On crit talent
	if hitted and crit and not target.dead and weapon and weapon.talent_on_crit and next(weapon.talent_on_crit) and not self.turn_procs.melee_talent then
		for tid, data in pairs(weapon.talent_on_crit) do
			if rng.percent(data.chance) then
				self.turn_procs.melee_talent = true
				self:forceUseTalent(tid, {ignore_cd=true, ignore_energy=true, force_target=target, force_level=data.level, ignore_ressources=true})
			end
		end
	end
	-- Shattering Impact
	if hitted and self:attr("shattering_impact") and (not self.shattering_impact_last_turn or self.shattering_impact_last_turn < game.turn) then
		local dam = dam * self.shattering_impact
		local invuln = target.invulnerable
		game.logSeen(target, "The shattering blow creates a shockwave!")
		target.invulnerable = 1 -- Target already hit, don't damage it twice
		self:project({type="ball", radius=1, selffire=false}, target.x, target.y, DamageType.PHYSICAL, dam)
		target.invulnerable = invuln
		self:incStamina(-15)
		self.shattering_impact_last_turn = game.turn
	-- Burst on Hit
	if hitted and weapon and weapon.burst_on_hit then
		for typ, dam in pairs(weapon.burst_on_hit) do
			if dam > 0 then
				self:project({type="ball", radius=1, friendlyfire=false}, target.x, target.y, typ, dam)
			end
		end
	end

	-- Critical Burst (generally more damage then burst on hit and larger radius)
	if hitted and crit and weapon and weapon.burst_on_crit then
		for typ, dam in pairs(weapon.burst_on_crit) do
			if dam > 0 then
				self:project({type="ball", radius=2, friendlyfire=false}, target.x, target.y, typ, dam)
			end
dg's avatar
dg committed
	-- Arcane Destruction
	if hitted and crit and weapon and self:knowTalent(self.T_ARCANE_DESTRUCTION) then
		local typ = rng.table{{DamageType.FIRE,"ball_fire"}, {DamageType.LIGHTNING,"ball_lightning_beam"}, {DamageType.ARCANE,"ball_arcane"}}
		self:project({type="ball", radius=2, friendlyfire=false}, target.x, target.y, typ[1], self:combatSpellpower() * 2)
		game.level.map:particleEmitter(target.x, target.y, 2, typ[2], {radius=2, tx=target.x, ty=target.y})
	-- Onslaught
	if hitted and self:attr("onslaught") then
dg's avatar
dg committed
		local dir = util.getDir(target.x, target.y, self.x, self.y) or 6
		local lx, ly = util.coordAddDir(self.x, self.y, util.dirSides(dir, self.x, self.y).left)
		local rx, ry = util.coordAddDir(self.x, self.y, util.dirSides(dir, self.x, self.y).right)
		local lt, rt = game.level.map(lx, ly, Map.ACTOR), game.level.map(rx, ry, Map.ACTOR)

dg's avatar
dg committed
		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"))
dg's avatar
dg committed
			target:crossTierEffect(target.EFF_OFFBALANCE, self:combatAttack())
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"))
dg's avatar
dg committed
			target:crossTierEffect(target.EFF_OFFBALANCE, self:combatAttack())
dg's avatar
dg committed
		if rt and rt:checkHit(self:combatAttack(weapon), rt:combatPhysicalResist(), 0, 95, 10) and rt:canBe("knockback") then
			rt:knockback(self.x, self.y, self:attr("onslaught"))
dg's avatar
dg committed
			target:crossTierEffect(target.EFF_OFFBALANCE, self:combatAttack())
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 type(dam) == "number" then if dam > 0 then DamageType:get(typ).projector(target, self.x, self.y, typ, dam) end
		elseif dam.dam and dam.dam > 0 then DamageType:get(typ).projector(target, self.x, self.y, typ, dam)
dg's avatar
dg committed
		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

	-- Bloodbath
	if hitted and crit and self:knowTalent(self.T_BLOODBATH) then
		local t = self:getTalentFromId(self.T_BLOODBATH)
		t.do_bloodbath(self, t)
	end

	-- Mortal Terror
	if hitted and not target.dead and self:knowTalent(self.T_MORTAL_TERROR) then
		local t = self:getTalentFromId(self.T_MORTAL_TERROR)
		t.do_terror(self, t, target, dam)
	end

dg's avatar
dg committed
	-- Dwarves stoneskin
	if hitted and not target.dead and target:attr("auto_stoneskin") and rng.percent(15) then
		target:setEffect(target.EFF_STONE_SKIN, 5, {power=target:attr("auto_stoneskin")})
dg's avatar
dg committed
	end

dg's avatar
dg committed
	-- Conduit (Psi)
	if hitted and not target.dead and self:knowTalent(self.T_CONDUIT) and self:isTalentActive(self.T_CONDUIT) and self.use_psi_combat then
		local t = self:getTalentFromId(self.T_CONDUIT)
dg's avatar
dg committed
		t.do_combat(self, t, target)
	end
dg's avatar
dg committed
	-- Exploit Weakness
	if hitted and not target.dead and self:knowTalent(self.T_EXPLOIT_WEAKNESS) and self:isTalentActive(self.T_EXPLOIT_WEAKNESS) then
		local t = self:getTalentFromId(self.T_EXPLOIT_WEAKNESS)
		t.do_weakness(self, t, target)
	end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Lacerating Strikes
	if hitted and not target.dead and self:isTalentActive(self.T_LACERATING_STRIKES) then
dg's avatar
dg committed
		local t = self:getTalentFromId(self.T_LACERATING_STRIKES)
		t.do_cut(self, t, target, dam)
	end

	-- Scoundrel's Strategies
	if hitted and not target.dead and self:knowTalent(self.T_SCOUNDREL) and target:hasEffect(target.EFF_CUT) then
		local t = self:getTalentFromId(self.T_SCOUNDREL)
		t.do_scoundrel(self, t, target)
	end

dg's avatar
dg committed
	-- Special effect
dg's avatar
dg committed
	if hitted and weapon and weapon.special_on_hit and weapon.special_on_hit.fct and (not target.dead or weapon.special_on_hit.on_kill) then
dg's avatar
dg committed
		weapon.special_on_hit.fct(weapon, self, target)
	end

dg's avatar
dg committed
	if hitted and crit and weapon and weapon.special_on_crit and weapon.special_on_crit.fct and (not target.dead or weapon.special_on_crit.on_kill) then
		weapon.special_on_crit.fct(weapon, self, target)
	end
dg's avatar
dg committed
	if hitted and weapon and weapon.special_on_kill and weapon.special_on_kill.fct and target.dead then
		weapon.special_on_kill.fct(weapon, self, target)
	end
DarkGod's avatar
DarkGod committed
	if hitted and crit and not target.dead and self:knowTalent(self.T_BACKSTAB) and not target:attr("stunned") and rng.percent(self:callTalent(self.T_BACKSTAB, "getStunChance")) then
		if target:canBe("stun") then
			target:setEffect(target.EFF_STUNNED, 3, {apply_power=self:combatAttack()})
		end
	end
dg's avatar
dg committed
	-- Poison coating
dg's avatar
dg committed
	if hitted and not target.dead and self.vile_poisons and next(self.vile_poisons) and target:canBe("poison") then
dg's avatar
dg committed
		local tid = rng.table(table.keys(self.vile_poisons))
		if tid then
			local t = self:getTalentFromId(tid)
			t.proc(self, t, target, weapon)
		end
	end

dg's avatar
dg committed
	-- Regen on being hit
dg's avatar
dg committed
	if hitted and not target.dead and target:attr("stamina_regen_when_hit") then target:incStamina(target.stamina_regen_when_hit) end
	if hitted and not target.dead and target:attr("mana_regen_when_hit") then target:incMana(target.mana_regen_when_hit) end
	if hitted and not target.dead and target:attr("equilibrium_regen_when_hit") then target:incEquilibrium(-target.equilibrium_regen_when_hit) end
	if hitted and not target.dead and target:attr("psi_regen_when_hit") then target:incPsi(target.psi_regen_when_hit) end
	if hitted and not target.dead and target:attr("hate_regen_when_hit") then target:incHate(target.hate_regen_when_hit) end
dg's avatar
dg committed
	-- Resource regen on hit
dg's avatar
dg committed
	if hitted and self:attr("stamina_regen_on_hit") then self:incStamina(self.stamina_regen_on_hit) end
	if hitted and self:attr("mana_regen_on_hit") then self:incMana(self.mana_regen_on_hit) end
dg's avatar
dg committed
	if hitted and not target.dead and target:knowTalent(target.T_STONESHIELD) then
		local t = target:getTalentFromId(target.T_STONESHIELD)
		local m, mm, e, em = t.getValues(self, t)
		target:incMana(math.min(dam * m, mm))
		target:incEquilibrium(-math.min(dam * e, em))
	end

dg's avatar
dg committed
	-- Ablative Armor
	if hitted and not target.dead and target:attr("carbon_spikes") then
		local t = target:getTalentFromId(target.T_CARBON_SPIKES)
		t.do_carbonLoss(target, t)
dg's avatar
dg committed
	end
dg's avatar
dg committed

dg's avatar
dg committed
	-- Set Up
	if not hitted and not target.dead and not evaded and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:hasEffect(target.EFF_DEFENSIVE_MANEUVER) then
		local t = target:getTalentFromId(target.T_SET_UP)
		local power = t.getPower(target, t)
		self:setEffect(self.EFF_SET_UP, 2, {src = target, power=power})
	end
dg's avatar
dg committed
	-- Counter Attack!
DarkGod's avatar
DarkGod committed
	if not hitted and not target.dead and target:knowTalent(target.T_COUNTER_ATTACK) and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:knowTalent(target.T_COUNTER_ATTACK) and self:isNear(target.x,target.y, 1) then --Adjacency check
		local cadam = target:callTalent(target.T_COUNTER_ATTACK,"checkCounterAttack")
		if cadam then
			game.logSeen(self, "%s counters the attack!", target.name:capitalize())
			target:attackTarget(self, nil, cadam, true)
		end 
	end 
	
	-- Gesture of Guarding counterattack
	if hitted and not target.dead and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:hasEffect(target.EFF_GESTURE_OF_GUARDING) then
		local t = target:getTalentFromId(target.T_GESTURE_OF_GUARDING)
		t.on_hit(target, t, self)
	end

	-- Defensive Throw!
DarkGod's avatar
DarkGod committed
	if not hitted and not target.dead and target:knowTalent(target.T_DEFENSIVE_THROW) and not target:attr("stunned") and not target:attr("dazed") and not target:attr("stoned") and target:isNear(self.x,self.y,1) then
		local t = target:getTalentFromId(target.T_DEFENSIVE_THROW)
		t.do_throw(target, self, t)
	end
	-- Greater Weapon Focus
	local gwf = self:hasEffect(self.EFF_GREATER_WEAPON_FOCUS)
	if hitted and not target.dead and weapon and gwf and not gwf.inside and rng.percent(gwf.chance) then
		gwf.inside = true
		game.logSeen(self, "%s focuses and gains an extra blow!", self.name:capitalize())
		self:attackTargetWith(target, weapon, damtype, mult)
		gwf.inside = nil
	end

dg's avatar
dg committed
	-- Zero gravity
	if hitted and game.level.data.zero_gravity and rng.percent(util.bound(dam, 0, 100)) then
dg's avatar
dg committed
		target:knockback(self.x, self.y, math.ceil(math.log(dam)))
	end
dg's avatar
dg committed
	-- Roll with it
	if hitted and target:attr("knockback_on_hit") and not target.turn_procs.roll_with_it and rng.percent(util.bound(dam, 0, 100)) then
		local ox, oy = self.x, self.y
		game:onTickEnd(function() 
			target:knockback(ox, oy, 1) 
			if not target:hasEffect(target.EFF_WILD_SPEED) then target:setEffect(target.EFF_WILD_SPEED, 1, {power=200}) end
		end)
dg's avatar
dg committed
		target.turn_procs.roll_with_it = true
	end

	-- Weakness hate bonus
	if hitted and effGloomWeakness and effGloomWeakness.hateBonus or 0 > 0 then
		self:incHate(effGloomWeakness.hateBonus)
		game.logPlayer(self, "#F53CBE#You revel in attacking a weakened foe! (+%d hate)", effGloomWeakness.hateBonus)
		effGloomWeakness.hateBonus = nil
	end
	-- Rampage
	if hitted and crit then
		local eff = self:hasEffect(self.EFF_RAMPAGE)
		if eff and not eff.critHit and eff.actualDuration < eff.maxDuration and self:knowTalent(self.T_BRUTALITY) then
			game.logPlayer(self, "#F53CBE#Your rampage is invigorated by your fierce attack! (+1 duration)")
			eff.critHit = true
			eff.actualDuration = eff.actualDuration + 1
			eff.dur = eff.dur + 1
		end
	end
	-- Marked Prey
	if hitted and not target.dead and effPredator and effPredator.type == target.type then
		if effPredator.subtype == target.subtype then
			-- Anatomy stun
			if effPredator.subtypeStunChance > 0 and rng.percent(effPredator.subtypeStunChance) then
				if target:canBe("stun") then
					target:setEffect(target.EFF_STUNNED, 3, {})
				else
					game.logSeen(target, "%s resists the stun!", target.name:capitalize())
				end
			end
			-- Outmaneuver
			if effPredator.subtypeOutmaneuverChance > 0 and rng.percent(effPredator.subtypeOutmaneuverChance) then
				local t = self:getTalentFromId(self.T_OUTMANEUVER)
				target:setEffect(target.EFF_OUTMANEUVERED, t.getDuration(self, t), { physicalResistChange=t.getPhysicalResistChange(self, t), statReduction=t.getStatReduction(self, t) })
			end
		else
			-- Outmaneuver
			if effPredator.typeOutmaneuverChance > 0 and rng.percent(effPredator.typeOutmaneuverChance) then
				local t = self:getTalentFromId(self.T_OUTMANEUVER)
dg's avatar
dg committed
				target:setEffect(target.EFF_OUTMANEUVERED, t.getDuration(self, t), { physicalResistChange=t.getPhysicalResistChange(self, t), statReduction=t.getStatReduction(self, t) })
	if hitted and crit and target:hasEffect(target.EFF_DISMAYED) then
		target:removeEffect(target.EFF_DISMAYED)
	end
	if hitted and not target.dead then
		-- Curse of Madness: Twisted Mind
		if self.hasEffect and self:hasEffect(self.EFF_CURSE_OF_MADNESS) then
			local eff = self:hasEffect(self.EFF_CURSE_OF_MADNESS)
			local def = self.tempeffect_def[self.EFF_CURSE_OF_MADNESS]
			def.doConspirator(self, eff, target)
		end
		if target.hasEffect and target:hasEffect(target.EFF_CURSE_OF_MADNESS) then
			local eff = target:hasEffect(target.EFF_CURSE_OF_MADNESS)
			local def = target.tempeffect_def[target.EFF_CURSE_OF_MADNESS]
			def.doConspirator(target, eff, self)
		end
		-- Curse of Nightmares: Suffocate
		if self.hasEffect and self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES) then
			local eff = self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES)
			local def = self.tempeffect_def[self.EFF_CURSE_OF_NIGHTMARES]
			def.doSuffocate(self, eff, target)
		end
		if target.hasEffect and target:hasEffect(target.EFF_CURSE_OF_NIGHTMARES) then
			local eff = target:hasEffect(target.EFF_CURSE_OF_NIGHTMARES)
			local def = target.tempeffect_def[target.EFF_CURSE_OF_NIGHTMARES]
			def.doSuffocate(target, eff, self)
		end
	end
dg's avatar
dg committed

	self:fireTalentCheck("callbackOnMeleeAttack", target, hitted, crit, weapon, damtype, mult, dam)

	local hd = {"Combat:attackTargetWith", hitted=hitted, crit=crit, target=target, weapon=weapon, damtype=damtype, mult=mult, dam=dam}
	if self:triggerHook(hd) then hitted = hd.hitted end

	-- Visual feedback
	if hitted then game.level.map:particleEmitter(target.x, target.y, 1, "melee_attack", {color=target.blood_color}) end
	self.turn_procs.weapon_type = nil
	self.__global_accuracy_damage_bonus = nil
	
	--Life Steal
	if weapon and weapon.lifesteal then
		self:attr("lifesteal", -weapon.lifesteal)
		self:attr("silent_heal", -1)
	end
dg's avatar
dg committed
	return self:combatSpeed(weapon), hitted
dg's avatar
dg committed
end

dg's avatar
dg committed
_M.weapon_talents = {
DarkGod's avatar
DarkGod committed
	sword =   "T_WEAPONS_MASTERY",
	axe =     "T_WEAPONS_MASTERY",
	mace =    "T_WEAPONS_MASTERY",
	knife =   "T_KNIFE_MASTERY",
	whip  =   "T_EXOTIC_WEAPONS_MASTERY",
	trident = "T_EXOTIC_WEAPONS_MASTERY",
	bow =     "T_BOW_MASTERY",
	sling =   "T_SLING_MASTERY",
	staff =   "T_STAFF_MASTERY",
	mindstar ="T_PSIBLADES",
	dream =   "T_DREAM_CRUSHER",
	unarmed = "T_UNARMED_MASTERY",
dg's avatar
dg committed
}

--- Static!
function _M:addCombatTraining(kind, tid)
	local wt = _M.weapon_talents
	if not wt[kind] then wt[kind] = tid return end

	if type(wt[kind]) == "table" then
		wt[kind][#wt[kind]+1] = tid
	else
		wt[kind] = { wt[kind] }
		wt[kind][#wt[kind]+1] = tid
	end
end

--- Checks weapon training
function _M:combatGetTraining(weapon)
	if not weapon then return nil end
	if not weapon.talented then return nil end
	if not _M.weapon_talents[weapon.talented] then return nil end
	if type(_M.weapon_talents[weapon.talented]) == "table" then
		local ktid, max = _M.weapon_talents[weapon.talented][1], self:getTalentLevel(_M.weapon_talents[weapon.talented][1])
		for i, tid in ipairs(_M.weapon_talents[weapon.talented]) do
			if self:knowTalent(tid) then 
				if self:getTalentLevel(tid) > max then
					ktid = tid
					max = self:getTalentLevel(tid)
				end
			end
		end
		return self:getTalentFromId(ktid)
	else
		return self:getTalentFromId(_M.weapon_talents[weapon.talented])
	end
dg's avatar
dg committed
--- Checks weapon training
function _M:combatCheckTraining(weapon)
dg's avatar
dg committed
	if not weapon then return 0 end
dg's avatar
dg committed
	if not weapon.talented then return 0 end
	if not _M.weapon_talents[weapon.talented] then return 0 end
	if type(_M.weapon_talents[weapon.talented]) == "table" then
		local max = 0
		for i, tid in ipairs(_M.weapon_talents[weapon.talented]) do
			max = math.max(max, self:getTalentLevel(tid))
		end
		return max
	else
		return self:getTalentLevel(_M.weapon_talents[weapon.talented])
	end
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the defense
--- Fake denotes a check not actually being made, used by character sheets etc.
dg's avatar
dg committed
function _M:combatDefenseBase(fake)
dg's avatar
dg committed
	local add = 0
DarkGod's avatar
DarkGod committed
	if not self:attr("encased_in_ice") then
		if self:hasDualWeapon() and self:knowTalent(self.T_DUAL_WEAPON_DEFENSE) then
			add = add + self:callTalent(self.T_DUAL_WEAPON_DEFENSE,"getDefense")
		end
		if not fake then
			add = add + (self:checkOnDefenseCall("defense") or 0)
		end
		if self:knowTalent(self.T_TACTICAL_EXPERT) then
			local t = self:getTalentFromId(self.T_TACTICAL_EXPERT)
			add = add + t.do_tact_update(self, t)
		end
		if self:knowTalent(self.T_CORRUPTED_SHELL) then
			add = add + self:getCon() / 3
		end
		if self:knowTalent(self.T_STEADY_MIND) then
			local t = self:getTalentFromId(self.T_STEADY_MIND)
			add = add + t.getDefense(self, t)
		end
		if self:isTalentActive(Talents.T_SURGE) then
			local t = self:getTalentFromId(self.T_SURGE)
			add = add + t.getDefenseChange(self, t)
		end
	local d = math.max(0, self.combat_def + (self:getDex() - 10) * 0.35 + (self:getLck() - 50) * 0.4)
	local mult = 1
dg's avatar
dg committed
	if self:hasLightArmor() and self:knowTalent(self.T_MOBILE_DEFENCE) then
		mult = mult + self:callTalent(self.T_MOBILE_DEFENCE,"getDef")
dg's avatar
dg committed
	end

dg's avatar
dg committed
	if self:knowTalent(self.T_MISDIRECTION) then
		mult = mult + self:callTalent(self.T_MISDIRECTION,"getDefense")/100
	return math.max(0, d * mult + add) -- Add bonuses last to avoid compounding defense multipliers from talents
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the defense ranged
function _M:combatDefense(fake, add)
dg's avatar
dg committed
	local base_defense = self:combatDefenseBase(true)
	if not fake then base_defense = self:combatDefenseBase() end
	local d = math.max(0, base_defense + (add or 0))
	if self:attr("dazed") then d = d / 2 end
dg's avatar
dg committed
	return self:rescaleCombatStats(d)
end

--- Gets the defense ranged
function _M:combatDefenseRanged(fake, add)
dg's avatar
dg committed
	local base_defense = self:combatDefenseBase(true)
	if not fake then base_defense = self:combatDefenseBase() end
	local d = math.max(0, base_defense + (self.combat_def_ranged or 0) + (add or 0))
	if self:attr("dazed") then d = d / 2 end
dg's avatar
dg committed
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
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_ARMOUR_TRAINING) then
dg's avatar
dg committed
		local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
		add = add + at.getArmor(self, at)
		if self:knowTalent(self.T_GOLEM_ARMOUR) then
			local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
			add = add + ga.getArmor(self, ga)
		end
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_CARBON_SPIKES) and self:isTalentActive(self.T_CARBON_SPIKES) then
		add = add + self.carbon_armor
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then
		add = add + self:callTalent(self.T_ARMOUR_OF_SHADOWS,"ArmourBonus")
dg's avatar
dg committed
	end
dg's avatar
dg committed
	return self.combat_armor + add
dg's avatar
dg committed
end

--- Gets armor hardiness
-- This is how much % of a blow we can reduce with armor
function _M:combatArmorHardiness()
	local add = 0
	if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then
dg's avatar
dg committed
		local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
		add = add + at.getArmorHardiness(self, at)
		if self:knowTalent(self.T_GOLEM_ARMOUR) then
			local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
			add = add + ga.getArmorHardiness(self, ga)
		end
dg's avatar
dg committed
	if self:hasLightArmor() and self:knowTalent(self.T_MOBILE_DEFENCE) then
		add = add + self:callTalent(self.T_MOBILE_DEFENCE, "getHardiness")
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_ARMOUR_OF_SHADOWS) and not game.level.map.lites(self.x, self.y) then
		add = add + 50
	end
dg's avatar
dg committed
	return util.bound(30 + self.combat_armor_hardiness + add, 0, 100)
dg's avatar
dg committed
--- Gets the attack
function _M:combatAttackBase(weapon, ammo)
dg's avatar
dg committed
	weapon = weapon or self.combat or {}
	return 4 + self.combat_atk + self:getTalentLevel(Talents.T_WEAPON_COMBAT) * 10 + (weapon.atk or 0) + (ammo and ammo.atk or 0) + (self:getLck() - 50) * 0.4
end
function _M:combatAttack(weapon, ammo)
dg's avatar
dg committed
	local stats
dg's avatar
dg committed
	if self.use_psi_combat then stats = self:getCun(100, true) - 10
	elseif weapon and weapon.wil_attack then stats = self:getWil(100, true) - 10
dg's avatar
dg committed
	else stats = self:getDex(100, true) - 10
dg's avatar
dg committed
	end
dg's avatar
dg committed
	local d = self:combatAttackBase(weapon, ammo) + stats
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
end

dg's avatar
dg committed
function _M:combatAttackRanged(weapon, ammo)
	local stats
	if self.use_psi_combat then stats = self:getCun(100, true) - 10
	elseif weapon and weapon.wil_attack then stats = self:getWil(100, true) - 10
	else stats = self:getDex(100, true) - 10
	end
dg's avatar
dg committed
	local d = self:combatAttackBase(weapon, ammo) + stats + (self.combat_atk_ranged or 0)
dg's avatar
dg committed
	if self:attr("dazed") then d = d / 2 end
dg's avatar
dg committed
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the attack using only strength
function _M:combatAttackStr(weapon, ammo)
dg's avatar
dg committed
	local d = self:combatAttackBase(weapon, ammo) + (self:getStr(100, true) - 10)
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
end

--- Gets the attack using only dexterity
function _M:combatAttackDex(weapon, ammo)
dg's avatar
dg committed
	local d = self:combatAttackBase(weapon, ammo) + (self:getDex(100, true) - 10)
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the attack using only magic
function _M:combatAttackMag(weapon, ammo)
dg's avatar
dg committed
	local d = self:combatAttackBase(weapon, ammo) + (self:getMag(100, true) - 10)
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d)
dg's avatar
dg committed
end

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

--- Gets the weapon speed
function _M:combatSpeed(weapon)
dg's avatar
dg committed
	weapon = weapon or self.combat or {}
	return (weapon.physspeed or 1) / math.max(self.combat_physspeed, 0.1)
dg's avatar
dg committed
end

--- Gets the crit rate
function _M:combatCrit(weapon)
dg's avatar
dg committed
	weapon = weapon or self.combat or {}
dg's avatar
dg committed
	local addcrit = 0
	if weapon.talented and self:knowTalent(Talents.T_LETHALITY) then
		addcrit = 1 + self:callTalent(Talents.T_LETHALITY, "getCriticalChance")
dg's avatar
dg committed
	end
	local crit = self.combat_physcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + (weapon.physcrit or 1) + addcrit
	return math.max(crit, 0) -- note: crit > 100% may be offset by crit reduction elsewhere
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the damage range
function _M:combatDamageRange(weapon)
dg's avatar
dg committed
	weapon = weapon or self.combat or {}
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
--- Scale damage values
dg's avatar
dg committed
-- This currently beefs up high-end damage values to make up for the combat stat rescale nerf.
dg's avatar
dg committed
function _M:rescaleDamage(dam)
	if dam <= 0 then return dam end
dg's avatar
dg committed
--	return dam * (1 - math.log10(dam * 2) / 7) --this is the old version, pre-combat-stat-rescale
	return dam ^ 1.04
end
dg's avatar
dg committed
--Diminishing-returns method of scaling combat stats, observing this rule: the first twenty ranks cost 1 point each, the second twenty cost two each, and so on. This is much, much better for players than some logarithmic mess, since they always know exactly what's going on, and there are nice breakpoints to strive for.
function _M:rescaleCombatStats(raw_combat_stat_value)
	local x = raw_combat_stat_value
DarkGod's avatar
DarkGod committed
	local tiers = 50 -- Just increase this if you want to add high-level content that allows for combat stat scores over 100.
dg's avatar
dg committed
	--return math.floor(math.min(x, 20) + math.min(math.max((x-20), 0)/2, 20) + math.min(math.max((x-60), 0)/3, 20) + math.min(math.max((x-120), 0)/4, 20) + math.min(math.max((x-200), 0)/5, 20)) --Five terms of the summation below.
	local total = 0
	for i = 1, tiers do
		local sub = 20*(i*(i-1)/2)
		total = total + math.min(math.max(x-sub, 0)/i, 20)
	end
	return total
dg's avatar
dg committed
end

dg's avatar
dg committed
-- Scale a value up or down by a power
-- x = a numeric value
-- y_low = value to match at x_low
-- y_high = value to match at x_high
-- power = scaling factor (default 0.5)
-- add = amount to add the result (default 0)
-- shift = amount to add to the input value before computation (default 0)
function _M:combatScale(x, y_low, x_low, y_high, x_high, power, add, shift)
	power, add, shift = power or 0.5, add or 0, shift or 0
	local x_low_adj, x_high_adj = (x_low+shift)^power, (x_high+shift)^power
dg's avatar
dg committed
	local m = (y_high - y_low)/(x_high_adj - x_low_adj)
	local b = y_low - m*x_low_adj
	return m * (x + shift)^power + b + add
dg's avatar
dg committed
end

-- Scale a value up or down subject to a limit
-- x = a numeric value
-- limit = value approached as x increases
-- y_high = value to match at when x = x_high
-- y_low (optional) = value to match when x = x_low
--	returns (limit - add)*x/(x + halfpoint) + add (= add when x = 0 and limit when x = infinity), halfpoint, add
-- halfpoint and add are internally computed to match the desired high/low values
-- note that the progression low->high->limit must be monotone, consistently increasing or decreasing
function _M:combatLimit(x, limit, y_low, x_low, y_high, x_high)
--	local x_low, x_high = 1,5 -- Implied talent levels for low and high values respectively
--	local tl = type(t) == "table" and (raw and self:getTalentLevelRaw(t) or self:getTalentLevel(t)) or t
	if y_low and x_low then
		local p = limit*(x_high-x_low)
		local m = x_high*y_high - x_low*y_low
		local halfpoint = (p-m)/(y_high - y_low)
		local add = (limit*(x_high*y_low-x_low*y_high) + y_high*y_low*(x_low-x_high))/(p-m)
		return (limit-add)*x/(x + halfpoint) + add
--		return (limit-add)*x/(x + halfpoint) + add, halfpoint, add
	else
		local add = 0
		local halfpoint = limit*x_high/(y_high-add)-x_high
		return (limit-add)*x/(x + halfpoint) + add
--		return (limit-add)*x/(x + halfpoint) + add, halfpoint, add
	end
end

-- Compute a diminishing returns value based on talent level that scales with a power
-- t = talent def table or a numeric value
-- low = value to match at talent level 1
-- high = value to match at talent level 5
-- power = scaling factor (default 0.5) or "log" for log10
-- add = amount to add the result (default 0)
-- shift = amount to add to the talent level before computation (default 0)
-- raw if true specifies use of raw talent level
function _M:combatTalentScale(t, low, high, power, add, shift, raw)
	local tl = type(t) == "table" and (raw and self:getTalentLevelRaw(t) or self:getTalentLevel(t)) or t
	power, add, shift = power or 0.5, add or 0, shift or 0
	local x_low, x_high = 1, 5 -- Implied talent levels to fit
	local x_low_adj, x_high_adj
	if power == "log" then
		x_low_adj, x_high_adj = math.log10(x_low+shift), math.log10(x_high+shift)
		tl = math.max(1, tl)
	else
		x_low_adj, x_high_adj = (x_low+shift)^power, (x_high+shift)^power
	end
	local m = (high - low)/(x_high_adj - x_low_adj)
	local b = low - m*x_low_adj
	if power == "log" then -- always >= 0
		return math.max(0, m * math.log10(tl + shift) + b + add)
--		return math.max(0, m * math.log10(tl + shift) + b + add), m, b
	else 
		return math.max(0, m * (tl + shift)^power + b + add)
--		return math.max(0, m * (tl + shift)^power + b + add), m, b
	end
end

-- Compute a diminishing returns value based on a stat value that scales with a power
-- stat == "str", "con",.... or a numeric value
-- low = value to match when stat = 10
-- high = value to match when stat = 100
-- power = scaling factor (default 0.5) or "log" for log10
-- add = amount to add the result (default 0)
-- shift = amount to add to the stat value before computation (default 0)
function _M:combatStatScale(stat, low, high, power, add, shift)
	stat = type(stat) == "string" and self:getStat(stat,nil,true) or stat
	power, add, shift = power or 0.5, add or 0, shift or 0
	local x_low, x_high = 10, 100 -- Implied stat values to match
	local x_low_adj, x_high_adj
	if power == "log" then
		x_low_adj, x_high_adj = math.log10(x_low+shift), math.log10(x_high+shift)
		stat = math.max(1, stat)
	else
		x_low_adj, x_high_adj = (x_low+shift)^power, (x_high+shift)^power
	end
	local m = (high - low)/(x_high_adj - x_low_adj)
	local b = low -m*x_low_adj
	if power == "log" then -- always >= 0
		return math.max(0, m * math.log10(stat + shift) + b + add)
--		return math.max(0, m * math.log10(stat + shift) + b + add), m, b
	else 
		return math.max(0, m * (stat + shift)^power + b + add)
--		return math.max(0, m * (stat + shift)^power + b + add), m, b
	end
end

-- Compute a diminishing returns value based on talent level that cannot go beyond a limit
-- t = talent def table or a numeric value
-- limit = value approached as talent levels increase
-- high = value at talent level 5
-- low = value at talent level 1 (optional)
-- raw if true specifies use of raw talent level
--	returns (limit - add)*TL/(TL + halfpoint) + add == add when TL = 0 and limit when TL = infinity
-- TL = talent level, halfpoint and add are internally computed to match the desired high/low values
-- note that the progression low->high->limit must be monotone, consistently increasing or decreasing
function _M:combatTalentLimit(t, limit, low, high, raw)
	local x_low, x_high = 1,5 -- Implied talent levels for low and high values respectively
	local tl = type(t) == "table" and (raw and self:getTalentLevelRaw(t) or self:getTalentLevel(t)) or t
	if low then
		local p = limit*(x_high-x_low)
		local m = x_high*high - x_low*low
		local halfpoint = (p-m)/(high - low)
		local add = (limit*(x_high*low-x_low*high) + high*low*(x_low-x_high))/(p-m)
		return (limit-add)*tl/(tl + halfpoint) + add
--		return (limit-add)*tl/(tl + halfpoint) + add, halfpoint, add
	else
		local add = 0
		local halfpoint = limit*x_high/(high-add)-x_high
		return (limit-add)*tl/(tl + halfpoint) + add
--		return (limit-add)*tl/(tl + halfpoint) + add, halfpoint, add
	end
end

-- Compute a diminishing returns value based on a stat value that cannot go beyond a limit
-- stat == "str", "con",.... or a numeric value
-- limit = value approached as talent levels increase
-- high = value to match when stat = 100
-- low = value to match when stat = 10 (optional)
--	returns (limit - add)*stat/(stat + halfpoint) + add == add when STAT = 0 and limit when stat = infinity
-- halfpoint and add are internally computed to match the desired high/low values
-- note that the progression low->high->limit must be monotone, consistently increasing or decreasing
function _M:combatStatLimit(stat, limit, low, high)
	local x_low, x_high = 10,100 -- Implied talent levels for low and high values respectively
	stat = type(stat) == "string" and self:getStat(stat,nil,true) or stat
	if low then
		local p = limit*(x_high-x_low)
		local m = x_high*high - x_low*low
		local halfpoint = (p-m)/(high - low)
		local add = (limit*(x_high*low-x_low*high) + high*low*(x_low-x_high))/(p-m)
		return (limit-add)*stat/(stat + halfpoint) + add
--		return (limit-add)*stat/(stat + halfpoint) + add, halfpoint, add
	else
		local add = 0
		local halfpoint = limit*x_high/(high-add)-x_high
		return (limit-add)*stat/(stat + halfpoint) + add
--		return (limit-add)*stat/(stat + halfpoint) + add, halfpoint, add
	end
end

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

dg's avatar
dg committed
	local sub_cun_to_str = false
	if weapon.talented and weapon.talented == "knife" and self:knowTalent(Talents.T_LETHALITY) then sub_cun_to_str = true end
dg's avatar
dg committed

dg's avatar
dg committed
	local dammod = weapon.dammod or {str=0.6}
	for stat, mod in pairs(dammod) do
dg's avatar
dg committed
		if sub_cun_to_str and stat == "str" then stat = "cun" end
dg's avatar
dg committed
		if self.use_psi_combat and stat == "str" then stat = "wil" end
		if self.use_psi_combat and stat == "dex" then stat = "cun" end
		totstat = totstat + self:getStat(stat) * mod
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self.use_psi_combat then
		if self:knowTalent(self.T_GREATER_TELEKINETIC_GRASP) then
			local g = self:getTalentFromId(self.T_GREATER_TELEKINETIC_GRASP)
			totstat = totstat * g.stat_sub(self, g)
		else
dg's avatar
dg committed
			totstat = totstat * 0.6
	if self:knowTalent(self.T_SUPERPOWER) then
		totstat = totstat + self:getStat("wil") * 0.3
	end

dg's avatar
dg committed
	if self:knowTalent(self.T_ARCANE_MIGHT) then
		totstat = totstat + self:getStat("mag") * 0.5
	end

	local talented_mod = math.sqrt(self:combatCheckTraining(weapon) / 5) / 2 + 1
dg's avatar
dg committed

dg's avatar
dg committed
	local power = math.max((weapon.dam or 1), 1)
	power = (math.sqrt(power / 10) - 1) * 0.5 + 1
	--print(("[COMBAT DAMAGE] power(%f) totstat(%f) talent_mod(%f)"):format(power, totstat, talented_mod))
	return self:rescaleDamage(0.3*(self:combatPhysicalpower(nil, weapon) + totstat) * power * talented_mod)
dg's avatar
dg committed
end

function _M:combatPhysicalpower(mod, weapon, add)
dg's avatar
dg committed
	mod = mod or 1
dg's avatar
dg committed
	if self:knowTalent(Talents.T_ARCANE_DESTRUCTION) then
		add = add + self:combatSpellpower() * self:callTalent(Talents.T_ARCANE_DESTRUCTION, "getSPMult")
dg's avatar
dg committed
	end
	if self:isTalentActive(Talents.T_BLOOD_FRENZY) then
		add = add + self.blood_frenzy
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_EMPTY_HAND) and self:isUnarmed() then
dg's avatar
dg committed
		local t = self:getTalentFromId(self.T_EMPTY_HAND)
		add = add + t.getDamage(self, t)
	end
	if self:attr("psychometry_power") then
		add = add + self:attr("psychometry_power")
	end

	if not weapon then
		local inven = self:getInven(self.INVEN_MAINHAND)
		if inven and inven[1] then weapon = self:getObjectCombat(inven[1], "mainhand") else weapon = self.combat end
dg's avatar
dg committed
	end

	add = add + 10 * self:combatCheckTraining(weapon)
	local d = math.max(0, self.combat_dam + add) + self:getStr() -- allows strong debuffs to offset strength
dg's avatar
dg committed
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d) * mod
dg's avatar
dg committed
end

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

dg's avatar
dg committed
--- Gets spellpower
function _M:combatSpellpower(mod, add)
dg's avatar
dg committed
	mod = mod or 1
	if self:knowTalent(self.T_ARCANE_CUNNING) then
		add = add + self:callTalent(self.T_ARCANE_CUNNING,"getSpellpower") * self:getCun() / 100
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_SHADOW_CUNNING) then
		add = add + self:callTalent(self.T_SHADOW_CUNNING,"getSpellpower") * self:getCun() / 100
dg's avatar
dg committed
	end
	if self:hasEffect(self.EFF_BLOODLUST) then
		add = add + self:hasEffect(self.EFF_BLOODLUST).power
dg's avatar
dg committed

	local am = 1
	if self:attr("spellpower_reduction") then am = 1 / (1 + self:attr("spellpower_reduction")) end

	local d = (self.combat_spellpower > 0 and self.combat_spellpower or 0) + add + self:getMag()
dg's avatar
dg committed
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d) * mod * am
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 self:rescaleDamage((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)
dg's avatar
dg committed
--	print("[TALENT WEAPON MULT]", self:getTalentLevel(t), base, max, t2, mult)
dg's avatar
dg committed
	return mult
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Gets the off hand multiplier
dg's avatar
dg committed
function _M:getOffHandMult(combat, mult)
	local offmult = 1/2
	-- Take the bigger multiplier from Dual weapon training and Corrupted Strength
dg's avatar
dg committed
	if self:knowTalent(Talents.T_DUAL_WEAPON_TRAINING) then
		offmult = math.max(offmult,self:callTalent(Talents.T_DUAL_WEAPON_TRAINING,"getoffmult"))
dg's avatar
dg committed
	end
	if self:knowTalent(Talents.T_CORRUPTED_STRENGTH) then
		offmult = math.max(offmult,self:callTalent(Talents.T_CORRUPTED_STRENGTH,"getoffmult"))
dg's avatar
dg committed
	end
	offmult = (mult or 1)*offmult
	if self:hasEffect(self.EFF_CURSE_OF_MADNESS) then
		local eff = self:hasEffect(self.EFF_CURSE_OF_MADNESS)
		if eff.level >= 1 and eff.unlockLevel >= 1 then
			local def = self.tempeffect_def[self.EFF_CURSE_OF_MADNESS]
			offmult = offmult + ((mult or 1) * def.getOffHandMultChange(eff.level) / 100)
		end
	end
	if combat and combat.no_offhand_penalty then
dg's avatar
dg committed
		return math.max(1, offmult)
	else
		return offmult
	end
dg's avatar
dg committed
--- Gets fatigue
function _M:combatFatigue()
	if self.fatigue < 0 then return 0 end
dg's avatar
dg committed
	if self:knowTalent(self.T_NO_FATIGUE) then return 0 end
dg's avatar
dg committed
	return self.fatigue
end

dg's avatar
dg committed
--- Gets spellcrit
function _M:combatSpellCrit()
	local crit = self.combat_spellcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + 1
dg's avatar
dg committed
	return util.bound(crit, 0, 100)
dg's avatar
dg committed
end

function _M:combatMindCrit(add)
	local add = add or 0
	if self:knowTalent(self.T_GESTURE_OF_POWER) then
		local t = self:getTalentFromId(self.T_GESTURE_OF_POWER)
		add = t.getMindCritChange(self, t)
	end

	local crit = self.combat_mindcrit + (self:getCun() - 10) * 0.3 + (self:getLck() - 50) * 0.30 + 1 + add
dg's avatar
dg committed
	return util.bound(crit, 0, 100)
dg's avatar
dg committed
--- Gets spellspeed
function _M:combatSpellSpeed()
	return 1 / math.max(self.combat_spellspeed, 0.1)
dg's avatar
dg committed
end
dg's avatar
dg committed

dg's avatar
dg committed
-- Gets mental speed
function _M:combatMindSpeed()
	return 1 / math.max(self.combat_mindspeed, 0.1)
dg's avatar
dg committed
end

--- Gets summon speed
function _M:combatSummonSpeed()
	return math.max(1 - ((self:attr("fast_summons") or 0) / 100), 0.1)
end

--- Computes physical crit chance reduction
function _M:combatCritReduction()
	local crit_reduction = 0
	if self:hasHeavyArmor() and self:knowTalent(self.T_ARMOUR_TRAINING) then
dg's avatar
dg committed
		local at = Talents:getTalentFromId(Talents.T_ARMOUR_TRAINING)
		crit_reduction = crit_reduction + at.getCriticalChanceReduction(self, at)
		if self:knowTalent(self.T_GOLEM_ARMOUR) then
			local ga = Talents:getTalentFromId(Talents.T_GOLEM_ARMOUR)
			crit_reduction = crit_reduction + ga.getCriticalChanceReduction(self, ga)
		end
	end
	if self:attr("combat_crit_reduction") then
		crit_reduction = crit_reduction + self:attr("combat_crit_reduction")
	end
	return crit_reduction
end

dg's avatar
dg committed
--- Computes physical crit for a damage
dg's avatar
dg committed
function _M:physicalCrit(dam, weapon, target, atk, def, add_chance, crit_power_add)
dg's avatar
dg committed
	self.turn_procs.is_crit = nil

dg's avatar
dg committed
	local tier_diff = self:getTierDiff(atk, def)
dg's avatar
dg committed

dg's avatar
dg committed
	local chance = self:combatCrit(weapon) + (add_chance or 0)
	crit_power_add = crit_power_add or 0
dg's avatar
dg committed

	if target:hasEffect(target.EFF_DISMAYED) then
		chance = 100
	end

dg's avatar
dg committed
	local crit = false
DarkGod's avatar
DarkGod committed
	if self:knowTalent(self.T_BACKSTAB) and target:attr("stunned") then chance = chance + self:callTalent(self.T_BACKSTAB,"getCriticalChance") end
dg's avatar
dg committed

	if target:attr("combat_crit_vulnerable") then
		chance = chance + target:attr("combat_crit_vulnerable")
dg's avatar
dg committed
	end
dg's avatar
dg committed
	if target:hasEffect(target.EFF_SET_UP) then
		local p = target:hasEffect(target.EFF_SET_UP)
		if p and p.src == self then
dg's avatar
dg committed
			chance = chance + p.power
		end
	end
dg's avatar
dg committed

	chance = chance - target:combatCritReduction()
dg's avatar
dg committed
	-- Scoundrel's Strategies
	if self:attr("cut") and target:knowTalent(self.T_SCOUNDREL) then
		chance = chance - target:callTalent(target.T_SCOUNDREL,"getCritPenalty")
	if self:attr("stealth") and self:knowTalent(self.T_SHADOWSTRIKE) and not target:canSee(self) then -- bug fix
		crit_power_add = crit_power_add + self:callTalent(self.T_SHADOWSTRIKE,"getMultiplier")
	if self:isAccuracyEffect(weapon, "axe") then
		local bonus = self:getAccuracyEffect(weapon, atk, def, 0.2, 10)
		print("[PHYS CRIT %] axe accuracy bonus", atk, def, "=", bonus)
		chance = chance + bonus
	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
		if target:hasEffect(target.EFF_OFFGUARD) then
			crit_power_add = crit_power_add + 0.1
dg's avatar
dg committed
		end

		if self:isAccuracyEffect(weapon, "sword") then
			local bonus = self:getAccuracyEffect(weapon, atk, def, 0.004, 0.25)
			print("[PHYS CRIT %] sword accuracy bonus", atk, def, "=", bonus)
			crit_power_add = crit_power_add + bonus
		end

dg's avatar
dg committed
		self.turn_procs.is_crit = "physical"
		self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dg's avatar
dg committed
		dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dg's avatar
dg committed
		crit = true
		if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "physical") end
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
dg's avatar
dg committed
function _M:spellCrit(dam, add_chance, crit_power_add)
dg's avatar
dg committed
	self.turn_procs.is_crit = nil

dg's avatar
dg committed
	crit_power_add = crit_power_add or 0
	local chance = self:combatSpellCrit() + (add_chance or 0)
dg's avatar
dg committed
	local crit = false
DarkGod's avatar
DarkGod committed
--	if self:attr("stealth") and self:knowTalent(self.T_SHADOWSTRIKE) and not target:canSee(self) then -- bug fix
	if self:attr("stealth") and self:knowTalent(self.T_SHADOWSTRIKE) then -- bug fix
		crit_power_add = crit_power_add + self:callTalent(self.T_SHADOWSTRIKE,"getMultiplier")
dg's avatar
dg committed
	print("[SPELL CRIT %]", chance)
dg's avatar
dg committed
	if rng.percent(chance) then
dg's avatar
dg committed
		self.turn_procs.is_crit = "spell"
dg's avatar
dg committed
		self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
		dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
dg's avatar
dg committed
		crit = true
		game.logSeen(self, "#{bold}#%s's spell attains critical power!#{normal}#", self.name:capitalize())
dg's avatar
dg committed

dg's avatar
dg committed
		if self:attr("mana_on_crit") then self:incMana(self:attr("mana_on_crit")) end
		if self:attr("vim_on_crit") then self:incVim(self:attr("vim_on_crit")) end
		if self:attr("paradox_on_crit") then self:incParadox(self:attr("paradox_on_crit")) end
		if self:attr("positive_on_crit") then self:incPositive(self:attr("positive_on_crit")) end
		if self:attr("negative_on_crit") then self:incNegative(self:attr("negative_on_crit")) end
		if self:attr("spellsurge_on_crit") then
			local power = self:attr("spellsurge_on_crit")
			self:setEffect(self.EFF_SPELLSURGE, 10, {power=power, max=power*3})
		end
dg's avatar
dg committed

dg's avatar
dg committed
		if self:isTalentActive(self.T_BLOOD_FURY) then
			local t = self:getTalentFromId(self.T_BLOOD_FURY)
			t.on_crit(self, t)
		end
dg's avatar
dg committed

		if self:isTalentActive(self.T_CORONA) then
			local t = self:getTalentFromId(self.T_CORONA)
dg's avatar
dg committed
			t.on_crit(self, t)

		if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "spell") end
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

--- Computes mind crit for a damage
dg's avatar
dg committed
function _M:mindCrit(dam, add_chance, crit_power_add)
dg's avatar
dg committed
	self.turn_procs.is_crit = nil

dg's avatar
dg committed
	crit_power_add = crit_power_add or 0
	local chance = self:combatMindCrit() + (add_chance or 0)
	local crit = false

DarkGod's avatar
DarkGod committed
--	if self:attr("stealth") and self:knowTalent(self.T_SHADOWSTRIKE) and not target:canSee(self) then -- bug fix
	if self:attr("stealth") and self:knowTalent(self.T_SHADOWSTRIKE) then -- bug fix
		crit_power_add = crit_power_add + self:callTalent(self.T_SHADOWSTRIKE,"getMultiplier")
	print("[MIND CRIT %]", chance)
	if rng.percent(chance) then
dg's avatar
dg committed
		self.turn_procs.is_crit = "mind"
dg's avatar
dg committed
		self.turn_procs.crit_power = (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
		dam = dam * (1.5 + crit_power_add + (self.combat_critical_power or 0) / 100)
		crit = true
		game.logSeen(self, "#{bold}#%s's mind surges with critical power!#{normal}#", self.name:capitalize())
		if self:attr("hate_on_crit") then self:incHate(self:attr("hate_on_crit")) end
dg's avatar
dg committed
		if self:attr("psi_on_crit") then self:incPsi(self:attr("psi_on_crit")) end
		if self:attr("equilibrium_on_crit") then self:incEquilibrium(self:attr("equilibrium_on_crit")) end
		if self:knowTalent(self.T_EYE_OF_THE_TIGER) then self:triggerTalent(self.T_EYE_OF_THE_TIGER, nil, "mind") end
dg's avatar
dg committed
		if self:knowTalent(self.T_LIVING_MUCUS) then self:callTalent(self.T_LIVING_MUCUS, "on_crit") end
	end
	return dam, crit
end

--- Do we get hit by our own AOE ?
function _M:spellFriendlyFire()
	local chance = (self:getLck() - 50) * 0.2
	if self:isTalentActive(self.T_SPELLCRAFT) then chance = chance + self:getTalentLevelRaw(self.T_SPELLCRAFT) * 20 end
dg's avatar
dg committed
	chance = chance + (self.combat_spell_friendlyfire or 0)

	chance = 100 - chance
	print("[SPELL] friendly fire chance", chance)
dg's avatar
dg committed
	return util.bound(chance, 0, 100)
--- Gets mindpower
	mod = mod or 1
	if self:knowTalent(self.T_SUPERPOWER) then
		add = add + 50 * self:getStr() / 100
	if self:knowTalent(self.T_GESTURE_OF_POWER) then
		local t = self:getTalentFromId(self.T_GESTURE_OF_POWER)
	if self:attr("psychometry_power") then
		add = add + self:attr("psychometry_power")
	end
	local d = (self.combat_mindpower > 0 and self.combat_mindpower or 0) + add + self:getWil() * 0.7 + self:getCun() * 0.4
dg's avatar
dg committed
	if self:attr("dazed") then d = d / 2 end
	return self:rescaleCombatStats(d) * 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
dg's avatar
dg committed
	return self:rescaleDamage((base + (self:combatMindpower())) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod)
--- Gets damage based on talent
function _M:combatTalentStatDamage(t, stat, base, max)
	-- Compute at "max"
	local mod = max / ((base + 100) * ((math.sqrt(5) - 1) * 0.8 + 1))
	-- Compute real
dg's avatar
dg committed
	local dam = (base + (self:getStat(stat))) * ((math.sqrt(self:getTalentLevel(t)) - 1) * 0.8 + 1) * mod
	dam =  dam * (1 - math.log10(dam * 2) / 7)
	dam = dam ^ (1 / 1.04)
	return self:rescaleDamage(dam)
dg's avatar
dg committed
--- Gets damage based on talent, basic stat, and interval
function _M:combatTalentIntervalDamage(t, stat, min, max, stat_weight)
	local stat_weight = stat_weight or 0.5
dg's avatar
dg committed
	local dam = min + (max - min)*((stat_weight * self:getStat(stat)/100) + (1 - stat_weight) * self:getTalentLevel(t)/6.5)
	dam =  dam * (1 - math.log10(dam * 2) / 7)
	dam = dam ^ (1 / 1.04)
	return self:rescaleDamage(dam)
end

--- Gets damage based on talent, stat, and interval
function _M:combatStatTalentIntervalDamage(t, stat, min, max, stat_weight)
	local stat_weight = stat_weight or 0.5
	scaled_stat = self[stat](self)
	return self:rescaleDamage(min + (max - min)*((stat_weight * self[stat](self)/100) + (1 - stat_weight) * self:getTalentLevel(t)/6.5))
dg's avatar
dg committed
end

dg's avatar
dg committed
--- Computes physical resistance
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatPhysicalResist(fake)
dg's avatar
dg committed
	local add = 0
dg's avatar
dg committed
	if not fake then
		add = add + (self:checkOnDefenseCall("physical") or 0)
	end
	if self:knowTalent(self.T_CORRUPTED_SHELL) then
		add = add + self:getCon() / 3
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_POWER_IS_MONEY) then
		add = add + self:callTalent(self.T_POWER_IS_MONEY, "getSaves")
dg's avatar
dg committed
	end
dg's avatar
dg committed
	-- To return later
	local d = self.combat_physresist + (self:getCon() + self:getStr() + (self:getLck() - 50) * 0.5) * 0.35 + add
	if self:attr("dazed") then d = d / 2 end
	local total = self:rescaleCombatStats(d)
dg's avatar
dg committed
	-- Psionic Balance
	if self:knowTalent(self.T_BALANCE) then
		local t = self:getTalentFromId(self.T_BALANCE)
		local ratio = t.getBalanceRatio(self, t)
		total = (1 - ratio)*total + self:combatMentalResist(fake)*ratio
	end
	return total
dg's avatar
dg committed
end

--- Computes spell resistance
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatSpellResist(fake)
dg's avatar
dg committed
	local add = 0
dg's avatar
dg committed
	if not fake then
		add = add + (self:checkOnDefenseCall("spell") or 0)
	end
	if self:knowTalent(self.T_CORRUPTED_SHELL) then
		add = add + self:getCon() / 3
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_POWER_IS_MONEY) then
		add = add + self:callTalent(self.T_POWER_IS_MONEY, "getSaves")
dg's avatar
dg committed
	end
dg's avatar
dg committed
	-- To return later
	local d = self.combat_spellresist + (self:getMag() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.35 + add
	if self:attr("dazed") then d = d / 2 end
	local total = self:rescaleCombatStats(d)
dg's avatar
dg committed
	-- Psionic Balance
	if self:knowTalent(self.T_BALANCE) then
		local t = self:getTalentFromId(self.T_BALANCE)
		local ratio = t.getBalanceRatio(self, t)
		total = (1 - ratio)*total + self:combatMentalResist(fake)*ratio
	end
	return total
dg's avatar
dg committed
end
dg's avatar
dg committed

--- Computes mental resistance
--- Fake denotes a check not actually being made, used by character sheets etc.
function _M:combatMentalResist(fake)
dg's avatar
dg committed
	local add = 0
dg's avatar
dg committed
	if not fake then
		add = add + (self:checkOnDefenseCall("mental") or 0)
	end
	if self:knowTalent(self.T_CORRUPTED_SHELL) then
		add = add + self:getCon() / 3
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_STEADY_MIND) then
		local t = self:getTalentFromId(self.T_STEADY_MIND)
		add = add + t.getMental(self, t)
	end
dg's avatar
dg committed
	if self:knowTalent(self.T_POWER_IS_MONEY) then
		add = add + self:callTalent(self.T_POWER_IS_MONEY, "getSaves")
dg's avatar
dg committed
	end

	local d = self.combat_mentalresist + (self:getCun() + self:getWil() + (self:getLck() - 50) * 0.5) * 0.35 + add
	if self:attr("dazed") then d = d / 2 end
	
	local nm = self:hasEffect(self.EFF_CURSE_OF_NIGHTMARES)
	if nm and rng.percent(20) then
		d = d * (1-self.tempeffect_def.EFF_CURSE_OF_NIGHTMARES.getVisionsReduction(nm, nm.level)/100)
	end	
	return self:rescaleCombatStats(d)
-- Called when a Save or Defense is checked
function _M:checkOnDefenseCall(type)
	local add = 0
	if self:knowTalent(self.T_SPIN_FATE) then
		print("Spin Fate", type)
		local t = self:getTalentFromId(self.T_SPIN_FATE)
		t.do_spin_fate(self, t, type)
	end
	return add
end

--- Returns the resistance
function _M:combatGetResist(type)
	local power = 100
	if self.force_use_resist and self.force_use_resist ~= type then 
		type = self.force_use_resist 
		power = self.force_use_resist_percent or 100
	end
dg's avatar
dg committed

	local a = math.min((self.resists.all or 0) / 100,1) -- Prevent large numbers from inverting the resist formulas
	local b = math.min((self.resists[type] or 0) / 100,1)
	local r = math.min(100 * (1 - (1 - a) * (1 - b)), (self.resists_cap.all or 0) + (self.resists_cap[type] or 0))
--- Returns the damage increase
function _M:combatHasDamageIncrease(type)
	if self.inc_damage[type] and self.inc_damage[type] ~= 0 then return true else return false end
end

--- Returns the damage increase
function _M:combatGetDamageIncrease(type, straight)
	local a = self.inc_damage.all or 0
	local b = self.inc_damage[type] or 0
	local inc = a + b
	if straight then return inc end

	if self.auto_highest_inc_damage and self.auto_highest_inc_damage[type] then
		local highest = self.inc_damage.all or 0
		for kind, v in pairs(self.inc_damage) do
			if kind ~= "all" then
				local inc = self:combatGetDamageIncrease(kind, true)
				highest = math.max(highest, inc)
			end
		end
		return highest + self.auto_highest_inc_damage[type]
	end

	return inc
end

dg's avatar
dg committed
--- Computes movement speed
function _M:combatMovementSpeed(x, y)
dg's avatar
dg committed
	local mult = 1
	if game.level and game.level.data.zero_gravity then
dg's avatar
dg committed
		mult = 3
	end
dg's avatar
dg committed

	local movement_speed = self.movement_speed
	if x and y and game.level.map:checkAllEntities(x, y, "creepingDark") and self:knowTalent(self.T_DARK_VISION) then
		local t = self:getTalentFromId(self.T_DARK_VISION)
		movement_speed = movement_speed + t.getMovementSpeedChange(self, t)
	end
	movement_speed = math.max(movement_speed, 0.1)
	return mult * (self.base_movement_speed or 1) / movement_speed
dg's avatar
dg committed
end

--- Computes see stealth
function _M:combatSeeStealth()
	local bonus = 0
	if self:knowTalent(self.T_PIERCING_SIGHT) then bonus = bonus + self:callTalent(self.T_PIERCING_SIGHT,"seePower") end
	if self:knowTalent(self.T_PRETERNATURAL_SENSES) then bonus = bonus + self:callTalent(self.T_PRETERNATURAL_SENSES, "sensePower") end
	-- level 50 with 100 cun ==> 50
	return self:combatScale(self.level/2 + self:getCun()/4 + (self:attr("see_stealth") or 0), 0, 0, 50, 50) + bonus -- Note bonus scaled separately from talents
end

--- Computes see invisible
function _M:combatSeeInvisible()
	local bonus = 0
	if self:knowTalent(self.T_PIERCING_SIGHT) then bonus = bonus + self:callTalent(self.T_PIERCING_SIGHT,"seePower") end
	if self:knowTalent(self.T_PRETERNATURAL_SENSES) then bonus = bonus + self:callTalent(self.T_PRETERNATURAL_SENSES, "sensePower") end
	return (self:attr("see_invisible") or 0) + bonus
end

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

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

	if not self:getInven("MAINHAND") then return end
	local weapon = self:getInven("MAINHAND")[1]
	if not weapon or (weapon.subtype ~= "battleaxe" and weapon.subtype ~= "waraxe") then
		return nil
	end
	return weapon
end

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

	if not self:getInven("MAINHAND") then return end
	local weapon = self:getInven("MAINHAND")[1]
	if not weapon then return nil end
	if type and weapon.combat.talented ~= type then return nil end
	return weapon
end

--- Check if the actor has a cursed weapon
function _M:hasCursedWeapon()
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

	if not self:getInven("MAINHAND") then return end
	local weapon = self:getInven("MAINHAND")[1]
	if not weapon or not weapon.curse then
	local t = self:getTalentFromId(self.T_DEFILING_TOUCH)
	if not t.canCurseItem(self, t, weapon) then return nil end
--- Check if the actor has a cursed weapon
function _M:hasCursedOffhandWeapon()
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

	if not self:getInven("OFFHAND") then return end
	local weapon = self:getInven("OFFHAND")[1]
	if not weapon or not weapon.combat or not weapon.curse then
		return nil
	end
	local t = self:getTalentFromId(self.T_DEFILING_TOUCH)
	if not t.canCurseItem(self, t, weapon) 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

dg's avatar
dg committed
-- Check if actor is unarmed
function _M:isUnarmed()
	local unarmed = true
	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 weapon or offweapon then
		unarmed = false
	end
	return unarmed
end

-- Get the number of free hands the actor has
function _M:getFreeHands()
dg's avatar
dg committed
	if not self:getInven("MAINHAND") or not self:getInven("OFFHAND") then return 0 end
	local weapon = self:getInven("MAINHAND")[1]
	local offweapon = self:getInven("OFFHAND")[1]
	if weapon and offweapon then return 0 end
dg's avatar
dg committed
	if weapon and weapon.twohanded then return 0 end
dg's avatar
dg committed
--- Check if the actor dual wields
DarkGod's avatar
DarkGod committed
function _M:hasDualWeapon(type)
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
DarkGod's avatar
DarkGod committed
	if type and weapon.combat.talented ~= type then return nil end
	if type and offweapon.combat.talented ~= type then return nil end
dg's avatar
dg committed
	return weapon, offweapon
end
dg's avatar
dg committed

--- Check if the actor uses psiblades
function _M:hasPsiblades(main, off)
	if self:attr("disarmed") then
		return nil, "disarmed"
	end

	local weapon, offweapon = nil, nil
	if main then
		if not self:getInven("MAINHAND") then return end
		weapon = self:getInven("MAINHAND")[1]
		if not weapon or not weapon.combat or not weapon.psiblade_active then return nil, "unactivated psiblade" end
	end
	if off then
		if not self:getInven("OFFHAND") then return end
		offweapon = self:getInven("OFFHAND")[1]
		if not offweapon or not offweapon.combat or not offweapon.psiblade_active then return nil, "unactivated psiblade" end
dg's avatar
dg committed
--- Check if the actor has a light armor
function _M:hasLightArmor()
	if not self:getInven("BODY") then return end
	local armor = self:getInven("BODY")[1]
	if not armor or (armor.subtype ~= "cloth" and armor.subtype ~= "light") then
		return nil
	end
	return armor
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" and armor.subtype ~= "massive") then
dg's avatar
dg committed
		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

dg's avatar
dg committed
--- Check if the actor has a cloak
function _M:hasCloak()
	if not self:getInven("CLOAK") then return end
	local cloak = self:getInven("CLOAK")[1]
	if not cloak then
		return nil
	end
	return cloak
end

dg's avatar
dg committed
-- Unarmed Combat; this handles grapple checks and building combo points
-- Builds Comob; reduces the cooldown on all unarmed abilities on cooldown by one
function _M:buildCombo()
	local duration = 5
dg's avatar
dg committed
	local power = 1
	-- Combo String bonuses
	if self:knowTalent(self.T_COMBO_STRING) then
		local t = self:getTalentFromId(self.T_COMBO_STRING)
dg's avatar
dg committed
		if rng.percent(t.getChance(self, t)) then
			power = 2
		end
		duration = 5 + t.getDuration(self, t)
dg's avatar
dg committed
	end
dg's avatar
dg committed

dg's avatar
dg committed
	if self:knowTalent(self.T_RELENTLESS_STRIKES) then
		local t = self:getTalentFromId(self.T_RELENTLESS_STRIKES)
dg's avatar
dg committed
		self:incStamina(t.getStamina(self, t))
dg's avatar
dg committed
	end
dg's avatar
dg committed
	self:setEffect(self.EFF_COMBO, duration, {power=power})
end

function _M:getCombo(combo)
	local combo = 0
	local p = self:hasEffect(self.EFF_COMBO)
dg's avatar
dg committed
		combo = p.cur_power
	end
		return combo
end

function _M:clearCombo()
	if self:hasEffect(self.EFF_COMBO) then
		self:removeEffect(self.EFF_COMBO)
	end
end

-- Check to see if the target is already being grappled; many talents have extra effects on grappled targets
function _M:isGrappled(source)
	local p = self:hasEffect(self.EFF_GRAPPLED)
	if p and p.src == source then
dg's avatar
dg committed
		return true
	else
		return false
	end
end

-- Breaks active grapples; called by a few talents that involve a lot of movement
function _M:breakGrapples()
	if self:hasEffect(self.EFF_GRAPPLING) then
		local p = self:hasEffect(self.EFF_GRAPPLING)
		if p.trgt then
			p.trgt:removeEffect(p.trgt.EFF_GRAPPLED)
		end
dg's avatar
dg committed
		self:removeEffect(self.EFF_GRAPPLING)
	end
end

-- grapple size check; compares attackers size and targets size
function _M:grappleSizeCheck(target)
	size = target.size_category - self.size_category
	if size > 1 then
DarkGod's avatar
DarkGod committed
		self:logCombat(target, "#Source#'s grapple fails because #Target# is too big!")
dg's avatar
dg committed
		return true
	else
		return false
	end
end

-- Starts the grapple
function _M:startGrapple(target)
	-- pulls boosted grapple effect from the clinch talent if known
	if self:knowTalent(self.T_CLINCH) then
		local t = self:getTalentFromId(self.T_CLINCH)
		power = t.getPower(self, t)
		duration = t.getDuration(self, t)
		hitbonus = self:getTalentLevel(t)/2
	else
		power = 5
		duration = 4
		hitbonus = 0
	end
	-- Breaks the grapple before reapplying
	if self:hasEffect(self.EFF_GRAPPLING) then
		self:removeEffect(self.EFF_GRAPPLING, true)
		target:setEffect(target.EFF_GRAPPLED, duration, {src=self, power=power}, true)
		self:setEffect(self.EFF_GRAPPLING, duration, {trgt=target}, true)
dg's avatar
dg committed
		return true
dg's avatar
dg committed
		target:setEffect(target.EFF_GRAPPLED, duration, {src=self, power=power, apply_power=self:combatPhysicalpower()})
		target:crossTierEffect(target.EFF_GRAPPLED, self:combatPhysicalpower())
		self:setEffect(self.EFF_GRAPPLING, duration, {trgt=target})
dg's avatar
dg committed
		return true
	else
		game.logSeen(target, "%s resists the grapple!", target.name:capitalize())
		return false
	end
DarkGod's avatar
DarkGod committed
-- Display Combat log messages, highlighting the player and taking LOS and visibility into account
-- #source#|#Source# -> <displayString> self.name|self.name:capitalize()
-- #target#|#Target# -> target.name|target.name:capitalize()
function _M:logCombat(target, style, ...)
	if not game.uiset or not game.uiset.logdisplay then return end
	local visible, srcSeen, tgtSeen = game:logVisible(self, target)  -- should a message be displayed?
	if visible then game.uiset.logdisplay(game:logMessage(self, srcSeen, target, tgtSeen, style, ...)) end 
end