From 4fa09e4294f043fe6164d3bcfbc36930ce847272 Mon Sep 17 00:00:00 2001
From: DarkGod <darkgod@net-core.org>
Date: Tue, 22 Oct 2013 22:52:52 +0200
Subject: [PATCH] NPCs can use Shoot Down

---
 .../default/engine/interface/ActorTalents.lua |  7 ++--
 game/modules/tome/ai/tactical.lua             | 40 +++++++++++++------
 game/modules/tome/class/Actor.lua             |  4 ++
 .../data/talents/techniques/excellence.lua    | 12 +++++-
 4 files changed, 47 insertions(+), 16 deletions(-)

diff --git a/game/engines/default/engine/interface/ActorTalents.lua b/game/engines/default/engine/interface/ActorTalents.lua
index e4b0a7b2aa..6d029b0d34 100644
--- a/game/engines/default/engine/interface/ActorTalents.lua
+++ b/game/engines/default/engine/interface/ActorTalents.lua
@@ -139,13 +139,14 @@ function _M:useTalent(id, who, force_level, ignore_cd, force_target, silent, no_
 			if not self:preUseTalent(ab, silent) then return end
 			local old_level
 			local old_target
-			
+
 			if force_level then old_level = who.talents[id]; who.talents[id] = force_level end
-			if force_target then old_target = rawget(who, "getTarget"); who.getTarget = function(a) return force_target.x, force_target.y, not force_target.__no_self and force_target end end
+			if ab.onAIGetTarget and not self.player then old_target = rawget(who, "getTarget"); who.getTarget = function() return ab.onAIGetTarget(self, ab) end end
+			if force_target and not old_target then old_target = rawget(who, "getTarget"); who.getTarget = function(a) return force_target.x, force_target.y, not force_target.__no_self and force_target end end
 			self.__talent_running = ab
 			local ok, ret, special = xpcall(function() return ab.action(who, ab) end, debug.traceback)
 			self.__talent_running = nil
-			if force_target then who.getTarget = old_target end
+			if old_target then who.getTarget = old_target end
 			if force_level then who.talents[id] = old_level end
 
 			if not ok then error(ret) end
diff --git a/game/modules/tome/ai/tactical.lua b/game/modules/tome/ai/tactical.lua
index b5cfc0e284..3cadfb7a80 100644
--- a/game/modules/tome/ai/tactical.lua
+++ b/game/modules/tome/ai/tactical.lua
@@ -54,18 +54,32 @@ newAI("use_tactical", function(self)
 	-- Find available talents
 	print("============================== TACTICAL AI", self.name)
 	local avail = {}
+	local _
 	local ok = false
-	local ax, ay = self:aiSeeTargetPos(self.ai_target.actor)
-	local target_dist = self.ai_target.actor and core.fov.distance(self.x, self.y, ax, ay)
-	local hate = self.ai_target.actor and (self:reactionToward(self.ai_target.actor) < 0)
-	local has_los = self.ai_target.actor and self:hasLOS(ax, ay)
+	local aitarget = self.ai_target.actor
+	local ax, ay = self:aiSeeTargetPos(aitarget)
+	local target_dist = aitarget and core.fov.distance(self.x, self.y, ax, ay)
+	local hate = aitarget and (self:reactionToward(aitarget) < 0)
+	local has_los = aitarget and self:hasLOS(ax, ay)
 	local self_compassion = (self.ai_state.self_compassion == false and 0) or self.ai_state.self_compassion or 5
 	local ally_compassion = (self.ai_state.ally_compassion == false and 0) or self.ai_state.ally_compassion or 1
 	for tid, lvl in pairs(self.talents) do
+		local aitarget = aitarget
+		local ax, ay = ax, ay
+		local target_dist = target_dist
 		local t = self:getTalentFromId(tid)
+
+		if t.onAIGetTarget then
+			_, _, aitarget = t.onAIGetTarget(self, t)
+			if aitarget then
+				ax, ay = self:aiSeeTargetPos(aitarget)
+				target_dist = aitarget and core.fov.distance(self.x, self.y, ax, ay)
+			end
+		end
+
 		local t_avail = false
-		print(self.name, self.uid, "tactical ai talents testing", t.name, tid)
-		if t.tactical then
+		print(self.name, self.uid, "tactical ai talents testing", t.name, tid, "on target", aitarget and aitarget.name)
+		if t.tactical and aitarget then
 			local tg = self:getTalentTarget(t)
 			local default_tg = {type=util.getval(t.direct_hit, self, t) and "hit" or "bolt"}
 			-- Only assume range... some talents may no require LOS, etc
@@ -88,7 +102,7 @@ newAI("use_tactical", function(self)
 				local self_hit = {}
 				local typ = engine.Target:getType(tg or default_tg)
 				if tg or self:getTalentRequiresTarget(t) then
-					local target_actor = self.ai_target.actor or self
+					local target_actor = aitarget or self
 					self:project(typ, ax, ay, function(px, py)
 						local act = game.level.map(px, py, engine.Map.ACTOR)
 						if act and not act.dead then
@@ -107,7 +121,7 @@ newAI("use_tactical", function(self)
 				end
 				-- Evaluate the tactical weights and weight functions
 				for tact, val in pairs(t.tactical) do
-					if type(val) == "function" then val = val(self, t, self.ai_target.actor) or 0 end
+					if type(val) == "function" then val = val(self, t, aitarget) or 0 end
 					-- Handle damage_types and resistances
 					local nb_foes_hit, nb_allies_hit, nb_self_hit = 0, 0, 0
 					if type(val) == "table" then
@@ -151,7 +165,7 @@ newAI("use_tactical", function(self)
 					val = val * (self.ai_talents and self.ai_talents[t.id] or 1) * (1 + lvl / 5)
 					-- Update the weight by the dummy projection data
 					-- Also force scaling if the talent requires a target (stand-in for canProject)
-					if self:getTalentRequiresTarget(t) or nb_foes_hit > 0 or nb_allies_hit > 0 or nb_self_hit > 0 then
+					if tact ~= "special" and (self:getTalentRequiresTarget(t) or nb_foes_hit > 0 or nb_allies_hit > 0 or nb_self_hit > 0) then
 						val = val * (nb_foes_hit - ally_compassion * nb_allies_hit - self_compassion * nb_self_hit)
 					end
 					-- Only take values greater than 0... allows the ai_talents to turn talents off
@@ -323,17 +337,17 @@ newAI("use_tactical", function(self)
 		end
 
 		-- Attacks
-		if avail.attack and self.ai_target.actor then
+		if avail.attack and aitarget then
 			-- Use the foe/ally ratio from the best attack talent
 			table.sort(avail.attack, function(a,b) return a.val > b.val end)
 			want.attack = avail.attack[1].val
 		end
-		if avail.disable and self.ai_target.actor then
+		if avail.disable and aitarget then
 			-- Use the foe/ally ratio from the best disable talent
 			table.sort(avail.disable, function(a,b) return a.val > b.val end)
 			want.disable = (want.attack or 0) + avail.disable[1].val
 		end
-		if avail.attackarea and self.ai_target.actor then
+		if avail.attackarea and aitarget then
 			-- Use the foe/ally ratio from the best attackarea talent
 			table.sort(avail.attackarea, function(a,b) return a.val > b.val end)
 			want.attackarea = avail.attackarea[1].val
@@ -344,6 +358,8 @@ newAI("use_tactical", function(self)
 			want.buff = math.max(0.01, want.attack + 0.5)
 		end
 
+		if avail.special then want.special = avail.special[1].val end
+
 		print("Tactical ai report for", self.name)
 		local res = {}
 		for k, v in pairs(want) do
diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index dd1a6e99b9..d4124cc27d 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -3769,6 +3769,10 @@ function _M:preUseTalent(ab, silent, fake)
 	if ab.is_teleport and self:attr("encased_in_ice") then return false end
 
 	end
+
+	-- Special checks -- AI
+	if not self.player and ab.on_pre_use_ai and not (ab.mode == "sustained" and self:isTalentActive(ab.id)) and not ab.on_pre_use_ai(self, ab, silent, fake) then return false end
+
 	if not silent then
 		-- Allow for silent talents
 		if ab.message ~= nil then
diff --git a/game/modules/tome/data/talents/techniques/excellence.lua b/game/modules/tome/data/talents/techniques/excellence.lua
index f76e5f5151..1b16e345ef 100644
--- a/game/modules/tome/data/talents/techniques/excellence.lua
+++ b/game/modules/tome/data/talents/techniques/excellence.lua
@@ -26,13 +26,23 @@ newTalent{
 	stamina = 20,
 	range = archery_range,
 	require = techs_dex_req_high1,
-	no_npc_use = true,
+	onAIGetTarget = function(self, t)
+		local tgts = {}
+		self:project({type="ball", radius=self:getTalentRange(t)}, self.x, self.y, function(px, py)
+			local tgt = game.level.map(px, py, Map.PROJECTILE)
+			if tgt and (not tgt.src or self:reactionToward(tgt.src) < 0) then tgts[#tgts+1] = {x=px, y=py, tgt=tgt, dist=core.fov.distance(self.x, self.y, px, py)} end
+		end)
+		table.sort(tgts, function(a, b) return a.dist < b.dist end)
+		if #tgts > 0 then return tgts[1].x, tgts[1].y, tgts[1].tgt end
+	end,
+	on_pre_use_ai = function(self, t, silent) return t.onAIGetTarget(self, t) and true or false end,
 	on_pre_use = function(self, t, silent) if not self:hasArcheryWeapon() then if not silent then game.logPlayer(self, "You require a bow or sling for this talent.") end return false end return true end,
 	requires_target = true,
 	getNb = function(self, t) return math.floor(self:combatTalentScale(t, 1, 5, "log")) end,
 	target = function(self, t)
 		return {type="bolt", range=self:getTalentRange(t), scan_on=engine.Map.PROJECTILE, no_first_target_filter=true}
 	end,
+	tactical = {SPECIAL=10},
 	action = function(self, t)
 		for i = 1, t.getNb(self, t) do
 			local targets = self:archeryAcquireTargets(self:getTalentTarget(t), {one_shot=true, no_energy=true})
-- 
GitLab