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