Skip to content
Snippets Groups Projects
Combat.lua 96.8 KiB
Newer Older
dg's avatar
dg committed
-- ToME - Tales of Maj'Eyal
DarkGod's avatar
DarkGod committed
-- Copyright (C) 2009 - 2016 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: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

			local energy = game.energy_to_act * self:combatMovementSpeed(x, y)
dg's avatar
dg committed
			if self:attr("bump_swap_speed_divide") then
				energy = energy / self:attr("bump_swap_speed_divide")
dg's avatar
dg committed
			end
			self:useEnergy(energy)
			self.did_energy = true
		end
	end
end

--- Makes the death happen!
dg's avatar
dg committed
--[[
The ToME combat system has the following attributes:
dg's avatar
dg committed
- attack: increases chances to hit against high defense
- defense: increases chances to miss against high attack power
dg's avatar
dg committed
- armor: direct reduction of damage done
- armor penetration: reduction of target's armor
- damage: raw damage done
]]
-- Attempts to attack a target with all melee weapons (calls self:attackTargetWith for each)
-- @param target - target actor
-- @param damtype a damage type ID <PHYSICAL>
-- @param mult a damage multiplier <1>
-- @noenergy if true the attack uses no energy
-- @force_unarmed if true the attacker uses unarmed (innate) combat parameters
-- @return true if an attack hit the target, false otherwise
function _M:attackTarget(target, damtype, mult, noenergy, force_unarmed)
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()
Eric Wykoff's avatar
Eric Wykoff committed
	self:breakSpacetimeTuning()
dg's avatar
dg committed
	if self:attr("feared") then
		if not noenergy then
dg's avatar
dg committed
			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

	if not target.turn_procs.warding_weapon and target:knowTalent(target.T_WARDING_WEAPON) and target:getTalentLevelRaw(target.T_WARDING_WEAPON) >= 5
HousePet's avatar
HousePet committed
		and rng.percent(target:callTalent(target.T_WARDING_WEAPON, "getChance")) then
		local t = self:getTalentFromId(self.T_WARDING_WEAPON)
		if target:getPsi() >= t.psi then
			target:setEffect(target.EFF_WEAPON_WARDING, 1, {})
			target.turn_procs.warding_weapon = true
			target:incPsi(-t.psi)
		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
Grayswandir's avatar
Grayswandir committed
			local damtype = table.get(gems[i], 'color_attributes', 'damage_type')
			if damtype then table.insert(types, damtype) end
dg's avatar
dg committed
		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
		if self.no_gesture_of_pain_recurse then return false end
		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
		self.no_gesture_of_pain_recurse = true
		self.no_gesture_of_pain_recurse = nil
	if not speed and not self:attr("disarmed") and not self:isUnarmed() and not force_unarmed then
		local double_weapon
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
					if o.double_weapon and not double_weapon then double_weapon = o end
					print("[ATTACK] attacking with (mainhand)", 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
		local oh_weaps, offhand = table.clone(self:getInven(self.INVEN_OFFHAND)) or {}, false
dg's avatar
dg committed
		-- Offhand attacks are with a damage penalty, that can be reduced by talents
		if double_weapon then oh_weaps[#oh_weaps+1] = double_weapon end -- use double weapon as OFFHAND if there are no others
		for i = 1, #oh_weaps do
			if i == #oh_weaps and double_weapon and offhand then break end
			local o = oh_weaps[i]
			local offmult = self:getOffHandMult(o.combat, mult)
			local combat = self:getObjectCombat(o, "offhand")
Loading
Loading full blame...