From c421493090e386a41aaaf99bb9f3d04ba6ccd942 Mon Sep 17 00:00:00 2001
From: grayswandir <grayswandir76@gmail.com>
Date: Fri, 28 Nov 2014 21:21:20 -0500
Subject: [PATCH] Added hooks and such for extending the targeting system.

---
 game/engines/default/engine/Target.lua        | 398 ++++++++++--------
 .../default/engine/interface/ActorProject.lua |  21 +-
 2 files changed, 242 insertions(+), 177 deletions(-)

diff --git a/game/engines/default/engine/Target.lua b/game/engines/default/engine/Target.lua
index 78bd6aab8e..2cdb1cbeb8 100644
--- a/game/engines/default/engine/Target.lua
+++ b/game/engines/default/engine/Target.lua
@@ -24,6 +24,8 @@ local Shader = require "engine.Shader"
 --- handles targetting
 module(..., package.seeall, class.make)
 
+_M.defaults = {}
+
 function _M:init(map, source_actor)
 	self.display_x, self.display_y = map.display_x, map.display_y
 	self.w, self.h = map.viewport.width, map.viewport.height
@@ -69,7 +71,7 @@ function _M:createTextures()
 end
 
 function _M:enableFBORenderer(texture, shader)
-	if not shader or not core.display.fboSupportsTransparency then 
+	if not shader or not core.display.fboSupportsTransparency then
 		self.fbo = nil
 		self:createTextures()
 		return
@@ -112,7 +114,7 @@ function _M:display(dispx, dispy, prevfbo, rotate_keyframes)
 	sx = sx + game.level.map.display_x
 	sy = sy + game.level.map.display_y
 	self.display_x, self.display_y = dispx or sx or self.display_x, dispy or sy or self.display_y
-	
+
 	if self.active then
 		if not self.fbo then
 			self:realDisplay(self.display_x, self.display_y)
@@ -149,11 +151,95 @@ function _M:display(dispx, dispy, prevfbo, rotate_keyframes)
 	self.display_x, self.display_y = ox, oy
 end
 
+-- Being completely blocked by the corner of an adjacent tile is annoying, so let's make it a special case and hit it instead.
+_M.defaults.display_blocked_by_adjacent = function(self, d)
+	if d.blocked_corner_x then
+		d.block = true
+		d.hit = true
+		d.hit_radius = false
+		stopped = true
+		if self.target_type.min_range and core.fov.distance(self.target_type.start_x, self.target_type.start_y, d.lx, d.ly) < self.target_type.min_range then
+			d.s = self.sr
+		end
+		if game.level.map:isBound(d.blocked_corner_x, d.blocked_corner_y) then
+			d.display_highlight(d.s, d.blocked_corner_x, d.blocked_corner_y)
+		end
+		d.s = self.sr
+	end
+end
+
+_M.defaults.display_check_block_path = function(self, d)
+	d.block, d.hit, d.hit_radius = false, true, true
+	d.block, d.hit, d.hit_radius = self.target_type:block_path(d.lx, d.ly, true)
+end
+
+-- Update coordinates and set color
+_M.defaults.display_update_hit = function(self, d)
+	if d.hit then
+		d.stop_x, d.stop_y = d.lx, d.ly
+		if not d.block and d.hit == "unknown" then d.s = self.sy end
+	else
+		d.s = self.sr
+	end
+end
+
+_M.defaults.display_update_radius = function(self, d)
+	if d.hit_radius then
+		d.stop_radius_x, d.stop_radius_y = d.lx, d.ly
+	end
+end
+
+_M.defaults.display_update_min_range = function(self, d)
+	if self.target_type.min_range then
+		-- Check if we should be "red"
+		if core.fov.distance(self.target_type.start_x, self.target_type.start_y, d.lx, d.ly) < self.target_type.min_range then
+			d.s = self.sr
+		-- Check if we were only "red" because of minimum distance
+		elseif d.s == self.sr then
+			d.s = self.sb
+		end
+	end
+end
+
+_M.defaults.display_line_step = function(self, d)
+	d.display_highlight(d.s, d.lx, d.ly)
+end
+
+_M.defaults.display_on_block = function(self, d)
+	d.s = self.sr
+	d.stopped = true
+end
+
+_M.defaults.display_on_block_corner = function(self, d)
+	d.block = true
+	d.stopped = true
+	d.hit_radius = false
+	d.s = self.sr
+	-- double the fun :-P
+	if game.level.map:isBound(d.blocked_corner_x, d.blocked_corner_y) then
+		if self.target_type.display_corner_block then
+			self.target_type.display_corner_block(self, d)
+		else
+			d.display_highlight(d.s, d.blocked_corner_x, d.blocked_corner_y, 2)
+		end
+	end
+end
+
+_M.defaults.display_default_target = function(self, d)
+	-- Entity tracking, if possible and if visible
+	if self.target.entity and self.target.entity.x and self.target.entity.y and game.level.map.seens(self.target.entity.x, self.target.entity.y) then
+		self.target.x, self.target.y = self.target.entity.x, self.target.entity.y
+	end
+	self.target.x = self.target.x or self.source_actor.x
+	self.target.y = self.target.y or self.source_actor.y
+end
+
 function _M:realDisplay(dispx, dispy, display_highlight)
 	if not display_highlight then
 		if util.isHex() then
 			display_highlight = function(texture, tx, ty, count)
 				count = count or 1
+				if self.target_type.filter and not self.target_type.no_filter_highlight and self.target_type.filter(tx, ty) then count = count + 1 end
 				for i = 1, count do
 					texture:toScreenHighlightHex(
 						dispx + (tx - game.level.map.mx) * self.tile_w * Map.zoom,
@@ -165,6 +251,7 @@ function _M:realDisplay(dispx, dispy, display_highlight)
 		else
 			display_highlight = function(texture, tx, ty, count)
 				count = count or 1
+				if self.target_type.filter and not self.target_type.no_filter_highlight and self.target_type.filter(tx, ty) then count = count + 1 end
 				for i = 1, count do
 					texture:toScreen(
 						dispx + (tx - game.level.map.mx) * self.tile_w * Map.zoom,
@@ -206,113 +293,70 @@ function _M:realDisplay(dispx, dispy, display_highlight)
 		return
 	end
 
+	local d = {}
+	d.display_highlight = display_highlight
+
 	-- Make sure we have a source
 	if not self.target_type.source_actor then
 		self.target_type.source_actor = self.source_actor
 	end
-	-- Entity tracking, if possible and if visible
-	if self.target.entity and self.target.entity.x and self.target.entity.y and game.level.map.seens(self.target.entity.x, self.target.entity.y) then
-		self.target.x, self.target.y = self.target.entity.x, self.target.entity.y
-	end
-	self.target.x = self.target.x or self.source_actor.x
-	self.target.y = self.target.y or self.source_actor.y
+
+	-- Pick default target
+	self.target_type.display_default_target(self, d)
 
 	self.target_type.start_x = self.target_type.start_x or self.target_type.x or self.target_type.source_actor and self.target_type.source_actor.x or self.x
 	self.target_type.start_y = self.target_type.start_y or self.target_type.y or self.target_type.source_actor and self.target_type.source_actor.y or self.y
 
-
 --	self.cursor:toScreen(dispx + (self.target.x - game.level.map.mx) * self.tile_w * Map.zoom, dispy + (self.target.y - game.level.map.my) * self.tile_h * Map.zoom, self.tile_w * Map.zoom, self.tile_h * Map.zoom)
 
 	-- Do not display if not requested
 	if not self.active then return end
 
-	local s = self.sb
-	local l
+	d.s = self.sb
 	if self.target_type.source_actor.lineFOV then
-		l = self.target_type.source_actor:lineFOV(self.target.x, self.target.y, nil, nil, self.target_type.start_x, self.target_type.start_y)
+		d.l = self.target_type.source_actor:lineFOV(self.target.x, self.target.y, nil, nil, self.target_type.start_x, self.target_type.start_y)
 	else
-		l = core.fov.line(self.target_type.start_x, self.target_type.start_y, self.target.x, self.target.y)
+		d.l = core.fov.line(self.target_type.start_x, self.target_type.start_y, self.target.x, self.target.y)
 	end
 	local block_corner = self.target_type.block_path and function(_, bx, by) local b, h, hr = self.target_type:block_path(bx, by, true) ; return b and h and not hr end
 		or function(_, bx, by) return false end
 
-	l:set_corner_block(block_corner)
-	local lx, ly, blocked_corner_x, blocked_corner_y = l:step()
+	d.l:set_corner_block(block_corner)
+	d.lx, d.ly, d.blocked_corner_x, d.blocked_corner_y = d.l:step()
 
-	local stop_x, stop_y = self.target_type.start_x, self.target_type.start_y
-	local stop_radius_x, stop_radius_y = self.target_type.start_x, self.target_type.start_y
-	local stopped = false
-	local block, hit, hit_radius
+	d.stop_x, d.stop_y = self.target_type.start_x, self.target_type.start_y
+	d.stop_radius_x, d.stop_radius_y = self.target_type.start_x, self.target_type.start_y
+	d.stopped = false
 
-	local firstx, firsty = lx, ly
+	d.firstx, d.firsty = d.lx, d.ly
 
 	-- Being completely blocked by the corner of an adjacent tile is annoying, so let's make it a special case and hit it instead
-	if blocked_corner_x then
-		block = true
-		hit = true
-		hit_radius = false
-		stopped = true
-		if self.target_type.min_range and core.fov.distance(self.target_type.start_x, self.target_type.start_y, lx, ly) < self.target_type.min_range then
-			s = self.sr
-		end
-		if game.level.map:isBound(blocked_corner_x, blocked_corner_y) then
-			display_highlight(s, blocked_corner_x, blocked_corner_y)
-		end
-		s = self.sr
-	end
-
-	while lx and ly do
-		if not stopped then
-			block, hit, hit_radius = false, true, true
-			if self.target_type.block_path then
-				block, hit, hit_radius = self.target_type:block_path(lx, ly, true)
-			end
+	self.target_type.display_blocked_by_adjacent(self, d)
 
+	while d.lx and d.ly do
+		if not d.stopped then
+			self.target_type.display_check_block_path(self, d)
 			-- Update coordinates and set color
-			if hit then
-				stop_x, stop_y = lx, ly
-				if not block and hit == "unknown" then s = self.sy end
-			else
-				s = self.sr
-			end
-			if hit_radius then
-				stop_radius_x, stop_radius_y = lx, ly
-			end
-			if self.target_type.min_range then
-				-- Check if we should be "red"
-				if core.fov.distance(self.target_type.start_x, self.target_type.start_y, lx, ly) < self.target_type.min_range then
-					s = self.sr
-				-- Check if we were only "red" because of minimum distance
-				elseif s == self.sr then
-					s = self.sb
-				end
-			end
-		end
-		display_highlight(s, lx, ly)
-		if block then
-			s = self.sr
-			stopped = true
+			self.target_type.display_update_hit(self, d)
+			self.target_type.display_update_radius(self, d)
+			self.target_type.display_update_min_range(self, d)
 		end
 
-		lx, ly, blocked_corner_x, blocked_corner_y = l:step()
+		self.target_type.display_line_step(self, d)
 
-		if blocked_corner_x and not stopped then
-			block = true
-			stopped = true
-			hit_radius = false
-			s = self.sr
-			-- double the fun :-P
-			if game.level.map:isBound(blocked_corner_x, blocked_corner_y) then
-				display_highlight(s, blocked_corner_x, blocked_corner_y, 2)
-			end
-		end
+		if d.block then self.target_type.display_on_block(self, d) end
+
+		d.lx, d.ly, d.blocked_corner_x, d.blocked_corner_y = d.l:step()
 
+		if d.blocked_corner_x and not d.stopped then
+			self.target_type.display_on_block_corner(self, d)
+		end
 	end
 
 	if self.target_type.ball and self.target_type.ball > 0 then
 		core.fov.calc_circle(
-			stop_radius_x,
-			stop_radius_y,
+			d.stop_radius_x,
+			d.stop_radius_y,
 			game.level.map.w,
 			game.level.map.h,
 			self.target_type.ball,
@@ -321,17 +365,19 @@ function _M:realDisplay(dispx, dispy, display_highlight)
 			end,
 			function(_, px, py)
 				if not self.target_type.no_restrict and not game.level.map.remembers(px, py) and not game.level.map.seens(px, py) then
-					display_highlight(self.syg, px, py)
+					d.display_highlight(self.syg, px, py)
 				else
-					display_highlight(self.sg, px, py)
+					d.display_highlight(self.sg, px, py)
 				end
 			end,
-		nil)
-	elseif self.target_type.cone and self.target_type.cone > 0 then
+			nil)
+	end
+
+	if self.target_type.cone and self.target_type.cone > 0 then
 		--local dir_angle = math.deg(math.atan2(self.target.y - self.source_actor.y, self.target.x - self.source_actor.x))
 		core.fov.calc_beam_any_angle(
-			stop_radius_x,
-			stop_radius_y,
+			d.stop_radius_x,
+			d.stop_radius_y,
 			game.level.map.w,
 			game.level.map.h,
 			self.target_type.cone,
@@ -345,16 +391,18 @@ function _M:realDisplay(dispx, dispy, display_highlight)
 			end,
 			function(_, px, py)
 				if not self.target_type.no_restrict and not game.level.map.remembers(px, py) and not game.level.map.seens(px, py) then
-					display_highlight(self.syg, px, py)
+					d.display_highlight(self.syg, px, py)
 				else
-					display_highlight(self.sg, px, py)
+					d.display_highlight(self.sg, px, py)
 				end
 			end,
 		nil)
-	elseif self.target_type.wall and self.target_type.wall > 0 then
+	end
+
+	if self.target_type.wall and self.target_type.wall > 0 then
 		core.fov.calc_wall(
-			stop_radius_x,
-			stop_radius_y,
+			d.stop_radius_x,
+			d.stop_radius_y,
 			game.level.map.w,
 			game.level.map.h,
 			self.target_type.wall,
@@ -368,15 +416,100 @@ function _M:realDisplay(dispx, dispy, display_highlight)
 			end,
 			function(_, px, py)
 				if not self.target_type.no_restrict and not game.level.map.remembers(px, py) and not game.level.map.seens(px, py) then
-					display_highlight(self.syg, px, py)
+					d.display_highlight(self.syg, px, py)
 				else
-					display_highlight(self.sg, px, py)
+					d.display_highlight(self.sg, px, py)
 				end
 			end,
 		nil)
 	end
+
+	d[1] = "Target:realDisplay"
+	self:triggerHook(d)
 end
 
+_M.defaults.block_path = function(typ, lx, ly, for_highlights)
+	if not game.level.map:isBound(lx, ly) then
+		return true, false, false
+	elseif not typ.no_restrict then
+		if typ.range and typ.start_x then
+			local dist = core.fov.distance(typ.start_x, typ.start_y, lx, ly)
+			if dist > typ.range then return true, false, false end
+		elseif typ.range and typ.source_actor and typ.source_actor.x then
+			local dist = core.fov.distance(typ.source_actor.x, typ.source_actor.y, lx, ly)
+			if dist > typ.range then return true, false, false end
+		end
+		local is_known = game.level.map.remembers(lx, ly) or game.level.map.seens(lx, ly)
+		if typ.requires_knowledge and not is_known then
+			return true, false, false
+		end
+		if not typ.pass_terrain and game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and not game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile") then
+			if for_highlights and not is_known then
+				return false, "unknown", true
+			else
+				return true, true, false
+			end
+		-- If we explode due to something other than terrain, then we should explode ON the tile, not before it
+		elseif typ.stop_block then
+			local nb = game.level.map:checkAllEntitiesCount(lx, ly, "block_move")
+			-- Reduce for pass_projectile or pass_terrain, which was handled above
+			if game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and (typ.pass_terrain or game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile")) then
+				nb = nb - 1
+			end
+			-- Reduce the nb blocking for friendlies
+			if not typ.friendlyblock and typ.source_actor and typ.source_actor.reactionToward then
+				local a = game.level.map(lx, ly, engine.Map.ACTOR)
+				if a and typ.source_actor:reactionToward(a) > 0 then
+					nb = nb - 1
+				end
+			end
+			if nb > 0 then
+				if for_highlights then
+					-- Targeting highlight should be yellow if we don't know what we're firing through
+					if not is_known then
+						return false, "unknown", true
+					-- Don't show the path as blocked if it's blocked by an actor we can't see
+					elseif nb == 1 and typ.source_actor and typ.source_actor.canSee and not typ.source_actor:canSee(game.level.map(lx, ly, engine.Map.ACTOR)) then
+						return false, true, true
+					end
+				end
+				return true, true, true
+			end
+		end
+		if for_highlights and not is_known then
+			return false, "unknown", true
+		end
+	end
+	-- If we don't block the path, then the explode point should be here
+	return false, true, true
+end
+
+_M.defaults.block_radius = function(typ, lx, ly, for_highlights)
+	return not typ.no_restrict and
+		game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and
+		not game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile") and
+		not (for_highlights and not (game.level.map.remembers(lx, ly) or game.level.map.seens(lx, ly)))
+end
+
+--- targeting type strings -> modification function.
+_M.types_def = {
+	ball = function(dest, src) dest.ball = src.radius end,
+	cone = function(dest, src)
+		dest.cone = src.radius
+		dest.cone_angle = src.cone_angle or 55
+		dest.selffire = false
+	end,
+	wall = function(dest, src)
+		if util.isHex() then
+			--with a hex grid, a wall should only be defined by the number of spots
+			src.halfmax_spots = src.halflength
+			src.halflength = 2 * src.halflength
+		end
+		dest.wall = src.halflength
+		end,
+	bolt = function(dest, src) dest.stop_block = true end,
+	beam = function(dest, scr) dest.line = true end,}
+
 -- @return t The target table used by ActorProject, Projectile, GameTargeting, etc.
 -- @param t Target table used to generate the
 -- @param t.type The engine-defined type, populates other more complex variables (see below)
@@ -416,92 +549,16 @@ function _M:getType(t)
 		selffire = true,
 		friendlyfire = true,
 		friendlyblock = true,
-		--- Determines how a path is blocked for a target type
-		--@param typ The target type table
-		block_path = function(typ, lx, ly, for_highlights)
-			if not game.level.map:isBound(lx, ly) then
-				return true, false, false
-			elseif not typ.no_restrict then
-				if typ.range and typ.start_x then
-					local dist = core.fov.distance(typ.start_x, typ.start_y, lx, ly)
-					if dist > typ.range then return true, false, false end
-				elseif typ.range and typ.source_actor and typ.source_actor.x then
-					local dist = core.fov.distance(typ.source_actor.x, typ.source_actor.y, lx, ly)
-					if dist > typ.range then return true, false, false end
-				end
-				local is_known = game.level.map.remembers(lx, ly) or game.level.map.seens(lx, ly)
-				if typ.requires_knowledge and not is_known then
-					return true, false, false
-				end
-				if not typ.pass_terrain and game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and not game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile") then
-					if for_highlights and not is_known then
-						return false, "unknown", true
-					else
-						return true, true, false
-					end
-				-- If we explode due to something other than terrain, then we should explode ON the tile, not before it
-				elseif typ.stop_block then
-					local nb = game.level.map:checkAllEntitiesCount(lx, ly, "block_move")
-					-- Reduce for pass_projectile or pass_terrain, which was handled above
-					if game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and (typ.pass_terrain or game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile")) then
-						nb = nb - 1
-					end
-					-- Reduce the nb blocking for friendlies
-					if not typ.friendlyblock and typ.source_actor and typ.source_actor.reactionToward then
-						local a = game.level.map(lx, ly, engine.Map.ACTOR)
-						if a and typ.source_actor:reactionToward(a) > 0 then
-							nb = nb - 1
-						end
-					end
-					if nb > 0 then
-						if for_highlights then
-							-- Targeting highlight should be yellow if we don't know what we're firing through
-							if not is_known then
-								return false, "unknown", true
-							-- Don't show the path as blocked if it's blocked by an actor we can't see
-							elseif nb == 1 and typ.source_actor and typ.source_actor.canSee and not typ.source_actor:canSee(game.level.map(lx, ly, engine.Map.ACTOR)) then
-								return false, true, true
-							end
-						end
-						return true, true, true
-					end
-				end
-				if for_highlights and not is_known then
-					return false, "unknown", true
-				end
-			end
-			-- If we don't block the path, then the explode point should be here
-			return false, true, true
-		end,
-		block_radius = function(typ, lx, ly, for_highlights)
-			return not typ.no_restrict and game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "block_move") and not game.level.map:checkEntity(lx, ly, engine.Map.TERRAIN, "pass_projectile") and not (for_highlights and not (game.level.map.remembers(lx, ly) or game.level.map.seens(lx, ly)))
-		end
 	}
+	for k, v in pairs(self.defaults) do target_type[k] = v end
 
 	-- And now modify for the default types
 	if t.type then
-		if t.type:find("ball") then
-			target_type.ball = t.radius
-		end
-		if t.type:find("cone") then
-			target_type.cone = t.radius
-			target_type.cone_angle = t.cone_angle or 55
-			target_type.selffire = false
-		end
-		if t.type:find("wall") then
-			if util.isHex() then
-				--with a hex grid, a wall should only be defined by the number of spots
-				t.halfmax_spots = t.halflength
-				t.halflength = 2*t.halflength
-			end
-			target_type.wall = t.halflength
-		end
-		if t.type:find("bolt") then
-			target_type.stop_block = true
-		elseif t.type:find("beam") then
-			target_type.line = true
+		for type_name, fun in pairs(self.types_def) do
+			if t.type:find(type_name) then fun(target_type, t) end
 		end
 	end
+
 	table.update(t, target_type)
 	return t
 end
@@ -551,7 +608,7 @@ function _M:scan(dir, radius, sx, sy, filter, kind)
 	sx = sx or self.target.x
 	sy = sy or self.target.y
 	if not sx or not sy then return end
-	
+
 	kind = kind or engine.Map.ACTOR
 	radius = radius or 20
 	local actors = {}
@@ -602,4 +659,3 @@ function _M:pointAtRange(srcx, srcy, destx, desty, dist)
 		return lx, ly
 	end
 end
-
diff --git a/game/engines/default/engine/interface/ActorProject.lua b/game/engines/default/engine/interface/ActorProject.lua
index 5f45b8376f..3e82304c65 100644
--- a/game/engines/default/engine/interface/ActorProject.lua
+++ b/game/engines/default/engine/interface/ActorProject.lua
@@ -56,6 +56,7 @@ function _M:project(t, x, y, damtype, dam, particles)
 
 	local grids = {}
 	local function addGrid(x, y)
+		if typ.filter and not typ.filter(x, y) then return end
 		if not grids[x] then grids[x] = {} end
 		grids[x][y] = true
 	end
@@ -112,7 +113,9 @@ function _M:project(t, x, y, damtype, dam, particles)
 		end
 	end
 
+	local single_target = true
 	if typ.ball and typ.ball > 0 then
+		single_target = false
 		core.fov.calc_circle(
 			stop_radius_x,
 			stop_radius_y,
@@ -128,7 +131,10 @@ function _M:project(t, x, y, damtype, dam, particles)
 			end,
 		nil)
 		addGrid(stop_x, stop_y)
-	elseif typ.cone and typ.cone > 0 then
+	end
+
+	if typ.cone and typ.cone > 0 then
+		single_target = false
 		--local dir_angle = math.deg(math.atan2(y - self.y, x - self.x))
 		core.fov.calc_beam_any_angle(
 			stop_radius_x,
@@ -149,7 +155,10 @@ function _M:project(t, x, y, damtype, dam, particles)
 			end,
 		nil)
 		addGrid(stop_x, stop_y)
-	elseif typ.wall and typ.wall > 0 then
+	end
+
+	if typ.wall and typ.wall > 0 then
+		single_target = false
 		core.fov.calc_wall(
 			stop_radius_x,
 			stop_radius_y,
@@ -168,11 +177,11 @@ function _M:project(t, x, y, damtype, dam, particles)
 				addGrid(px, py)
 			end,
 		nil)
-	else
-		-- Deal damage: single
-		addGrid(stop_x, stop_y)
 	end
 
+	-- Deal damage: single
+	if single_target then addGrid(stop_x, stop_y) end
+
 	-- Check for minimum range
 	if typ.min_range and core.fov.distance(typ.start_x, typ.start_y, stop_x, stop_y) < typ.min_range then
 		return
@@ -188,7 +197,7 @@ function _M:project(t, x, y, damtype, dam, particles)
 			end
 		end
 	end
-	
+
 	self:check("on_project_grids", grids)
 
 	-- Now project on each grid, one type
-- 
GitLab