GameTargeting.lua 17.4 KB
-- TE4 - T-Engine 4
-- Copyright (C) 2009 - 2019 Nicolas Casalini
--
-- 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

require "engine.class"
require "engine.KeyBind"
local Dialog = require "engine.ui.Dialog"
local Map = require "engine.Map"
local Target = require "engine.Target"

--- Handles default targeting interface & display
-- @classmod engine.generator.interface.GameTargeting
module(..., package.seeall, class.make)

--- Initializes targeting
function _M:init()
	self.target = Target.new(Map, self.player)
	self.target.target.entity = self.player
	self.old_tmx, self.old_tmy = 0, 0
	self.target_style = "lock"

	-- Allow scrolling when targetting
	self.target.on_set_target = function(self, how)
		if self.target and self.target_type and self.target.x and self.active and self.target_type.stop_before_target and how == "scan" then
			local 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
			local 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
			local l = core.fov.line(self.target.x, self.target.y, start_x, start_y)
			local lx, ly = l:step()
			if lx and ly then
				self.target.x = lx
				self.target.y = ly
				self.target.entity = game.level.map(self.target.x, self.target.y, engine.Map.ACTOR)
			end
		end

		if self.key ~= self.targetmode_key then return end
		local dx, dy = game.level.map:moveViewSurround(self.target.x, self.target.y, 1, 1, true)
		if how == "mouse" and (dx ~= 0 or dy ~= 0) then
			local cx, cy = core.mouse.get()
			core.mouse.set(cx - game.level.map.tile_w * dx, cy - game.level.map.tile_h * dy)
		end
	end
end

--- Maintain the current target each tick
-- Make sure the target still exists
function _M:targetOnTick()
	if self.target.target.entity and not self.level:hasEntity(self.target.target.entity) then self.target.target.entity = false end
end

--- Display the tooltip, if any
function _M:targetDisplayTooltip(dx, dy, force, nb_keyframes)
	-- Tooltip is displayed over all else
	if self.level and self.level.map and self.level.map.finished then
		local tmx, tmy
		-- Display a tooltip if available
		if self.tooltip_x then
			if type(self.tooltip_x) == "table" then
				self.tooltip:toScreen(self.tooltip.last_display_x, self.tooltip.last_display_y, nb_keyframes)
			else
				tmx, tmy = self.level.map:getMouseTile(self.tooltip_x , self.tooltip_y)
				self.tooltip:displayAtMap(tmx, tmy, dx, dy, nil, force, nb_keyframes)
			end
		end

		-- Move target around
		if self.old_tmx ~= tmx or self.old_tmy ~= tmy then
			if not (self.target_mode and self.target_no_move_tooltip) then
				self.target.target.x, self.target.target.y = tmx, tmy
			end
		end
		self.old_tmx, self.old_tmy = tmx, tmy
	end
end

--- Forces the tooltip to pop with the given text
function _M:tooltipDisplayAtMap(x, y, text, extra, force, nb_keyframes)
	self.tooltip:displayAtMap(nil, nil, x, y, text, force, nb_keyframes)
	if extra and type(extra) == "table" then
		if extra.up then self.tooltip.last_display_y = self.tooltip.last_display_y - self.tooltip.h end
	end
	self.tooltip_x = {}
end

--- Forces to hide the tooltip
function _M:tooltipHide()
	self.tooltip_x = nil
end

--- Enter/leave targeting mode
-- This is the "meat" of this interface, do not expect to understand it easily, it mixes some nasty stuff
-- This require the Game to have both a "key" field (this is the default) and a "normal_key" field<br/>
-- It will switch over to a special keyhandler and then restore the "normal_key" one
function _M:targetMode(v, msg, co, typ)
	local old = self.target_mode
	self.target_mode = v

	if not v then
		Map:setViewerFaction((self.always_target == true or self.always_target == "old") and self.player.faction or nil)
		if msg then self.log(type(msg) == "string" and msg or _t"Tactical display disabled. Press shift+'t' to enable.") end
		self.level.map.changed = true
		self.targetmode_trigger_hotkey = nil
		self.target:setActive(false)

		if tostring(old) == "exclusive" then
			local x, y, e = self.target.target.x, self.target.target.y, self.target.target.entity
			local fct = function(notok)
				if notok then
					self.target.target.entity = nil
					self.target.target.x = nil
					self.target.target.y = nil
					x, y, e = nil, nil, nil
				end

				self.key = self.normal_key
				self.key:setCurrent()
				if self.target_co then
					local co = self.target_co
					self.target_co = nil
					self.target.target.x, self.target.target.y, self.target.target.entity = x, y, e
					local ok, err = coroutine.resume(co, self.target.target.x, self.target.target.y, self.target.target.entity)
					if not ok and err then print(debug.traceback(co)) error(err) end
				end
			end
			if self.target_warning and self.target.target.x == self.player.x and self.target.target.y == self.player.y then
				Dialog:yesnoPopup(type(self.target_warning) == "string" and self.target_warning or _t"Target yourself?", _t"Are you sure you want to target yourself?", fct, _t"No", _t"Yes", nil, true)
			else
				fct(false)
			end
		end
	else
		Map:setViewerFaction(self.player.faction)
		if msg then self.log(type(msg) == "string" and msg or _t"Tactical display enabled. Press shift+'t' to disable.") end
		self.level.map.changed = true
		self.target:setActive(true, typ)
		self.target_style = "lock"
		self.target_warning = true
		self.target_no_move_tooltip = true
		if type(typ) == "table" and typ.no_move_tooltip then
			self.target_no_move_tooltip = true
		end
		if type(typ) == "table" and typ.talent then
			self.target_warning = typ.talent.name
		elseif type(typ) == "table" and typ.__name then
			self.target_warning = typ.__name
		end

		-- Exclusive mode means we disable the current key handler and use a specific one
		-- that only allows targetting and resumes talent coroutine when done
		if tostring(v) == "exclusive" then
			self.target_co = co
			self.key = self.targetmode_key
			self.key:setCurrent()

			local do_scan = true
			if type(typ) == "table" and typ.no_start_scan then
				do_scan = false
			else
				if self.target_no_start_scan
				   or (
				       self.target.target.entity and
				       self.level.map.seens(self.target.target.entity.x, self.target.target.entity.y) and
				       self.player ~= self.target.target.entity
				      ) then

					if type(typ) == "table" and typ.first_target ~= "friend" and self.target.target and self.target.target.entity and self.player:reactionToward(self.target.target.entity) >= 0 then
					else
						do_scan = false
					end
				end
			end
			if do_scan then
				local filter = nil
				if typ.custom_scan_filter then
					filter = typ.custom_scan_filter
				elseif not (type(typ) == "table" and typ.no_first_target_filter) then
					if type(typ) == "table" and typ.first_target and typ.first_target == "friend" then
						filter = function(a) return self.player:reactionToward(a) >= 0 end
					else
						filter = function(a) return self.player:reactionToward(a) < 0 end
					end
				end
				self.target:scan(5, nil, self.player.x, self.player.y, filter, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on)
			end
			if self.target.target.entity and self.target.target.entity.x and self.target.target.entity.y then self.target.target.x, self.target.target.y = self.target.target.entity.x, self.target.target.entity.y end
		end
		if self.target.target.x then
			self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y)
		end
	end
end

function _M:targetTriggerHotkey(i)
	self.targetmode_trigger_hotkey = i
end

--- This setups the default keybindings for targeting
function _M:targetSetupKey()
	local accept = function() self:targetMode(false, false) self.tooltip_x, self.tooltip_y = nil, nil end

	self.targetmode_key = engine.KeyBind.new()
	self.targetmode_key:addCommands{ _SPACE=accept, [{"_SPACE","ctrl"}]=accept, [{"_RETURN","ctrl"}]=accept, [{"_KP_ENTER","ctrl"}]=accept }
	
	if engine.interface and engine.interface.PlayerHotkeys then
		engine.interface.PlayerHotkeys:bindAllHotkeys(self.targetmode_key, function(i)
			if self.targetmode_trigger_hotkey == i then accept() end
		end)
	end

	self.targetmode_key:addBinds
	{
		TACTICAL_DISPLAY = accept,
		ACCEPT = accept,
		EXIT = function()
			self.target.target.entity = nil
			self.target.target.x = nil
			self.target.target.y = nil
			self:targetMode(false, false)
			self.tooltip_x, self.tooltip_y = nil, nil
		end,
		-- Targeting movement
		RUN_LEFT = function() self.target:freemove(4) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_RIGHT = function() self.target:freemove(6) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_UP = function() self.target:freemove(8) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_DOWN = function() self.target:freemove(2) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_LEFT_DOWN = function() self.target:freemove(1) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_RIGHT_DOWN = function() self.target:freemove(3) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_LEFT_UP = function() self.target:freemove(7) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		RUN_RIGHT_UP = function() self.target:freemove(9) self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,

		MOVE_LEFT = function() if self.target_style == "lock" then self.target:scan(4, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(4, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(4) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_RIGHT = function() if self.target_style == "lock" then self.target:scan(6, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(6, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(6) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_UP = function() if self.target_style == "lock" then self.target:scan(8, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(8, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(8) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_DOWN = function() if self.target_style == "lock" then self.target:scan(2, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(2, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(2) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_LEFT_DOWN = function() if self.target_style == "lock" then self.target:scan(1, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(1, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(1) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_RIGHT_DOWN = function() if self.target_style == "lock" then self.target:scan(3, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(3, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(3) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_LEFT_UP = function() if self.target_style == "lock" then self.target:scan(7, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(7, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(7) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_RIGHT_UP = function() if self.target_style == "lock" then self.target:scan(9, nil, nil, nil, nil, self.target.target_type and type(self.target.target_type) == "table" and self.target.target_type.scan_on) elseif self.target_style == "immediate" then self.target:setDirFrom(9, self.target.target.entity or self.player) self.targetmode_key:triggerVirtual("ACCEPT") return else self.target:freemove(9) end self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y) end,
		MOVE_STAY = function()
			if self.target_style == "immediate" then 
				self.target:setDirFrom(5, self.target.target.entity or self.player)
				self.targetmode_key:triggerVirtual("ACCEPT")
				return
			end
			self.target:setSpot(self.target.source_actor.x, self.target.source_actor.y, "freemove")
			self.tooltip_x, self.tooltip_y = self.level.map:getTileToScreen(self.target.target.x, self.target.target.y)
		end,
		SCREENSHOT = function() self.normal_key:triggerVirtual("SCREENSHOT") end,
	}
end

--- Handle mouse event for targeting
-- @return true if the event was handled
function _M:targetMouse(button, mx, my, xrel, yrel, event)
	if not self.level then return end

	-- Move tooltip
	self.tooltip_x, self.tooltip_y = mx, my
	local tmx, tmy = self.level.map:getMouseTile(mx, my)
	self.target:setSpot(tmx, tmy, "mouse")
	if self.key == self.targetmode_key then
		-- Target with mouse
		if button == "none" and xrel and yrel and event == "motion" then
			self.target:setSpotInMotion(tmx, tmy, "mouse")
		-- Accept target
		elseif button == "left" and not xrel and not yrel and event == "button" then
			self:targetMode(false, false)
			self.tooltip_x, self.tooltip_y = nil, nil
		-- Cancel target
		elseif not xrel and not yrel and event == "button" then
			self.target.target.entity = nil
			self.target.target.x = nil
			self.target.target.y = nil
			self:targetMode(false, false)
			self.tooltip_x, self.tooltip_y = nil, nil
		end
		return true
	end
end

--- Player requests a target
-- This method should be called by your Player:getTarget() method, it will handle everything
-- @param typ the targeting parameters
function _M:targetGetForPlayer(typ)
	if self.target.forced then return unpack(self.target.forced) end
	if coroutine.running() and typ then
		local msg
		self.target_no_start_scan = nil
		if type(typ) == "string" then msg, typ = typ, nil
		elseif type(typ) == "table" then
			if typ.default_target then
				self.target.target.entity = typ.default_target
				self.target_no_start_scan = true
			end
			msg = typ.msg
		end
		self:targetMode("exclusive", msg, coroutine.running(), typ)
		if typ.nowarning then self.target_warning = false end
		if self.target.target.x and config.settings.auto_accept_target and not typ.immediate_keys and (not typ.nolock or typ.can_autoaccept) and (not typ.nowarning or typ.can_autoaccept) and (not typ.no_restrict or typ.can_autoaccept) then
			self.target_co = nil
			self:targetMode(false, false) self.tooltip_x, self.tooltip_y = nil, nil
			return self.target.target.x, self.target.target.y, self.target.target.entity
		end
		if typ.immediate_keys then self.target_style = "immediate" end
		if typ.nolock then self.target_style = "free" end
		return coroutine.yield()
	end
	return self.target.target.x, self.target.target.y, self.target.target.entity
end

--- Player wants to set its target
-- This method should be called by your Player:setTarget() method, it will handle everything
function _M:targetSetForPlayer(target)
	self.target.target.entity = target
	self.target.target.x = (type(target) == "table" and target.x) or nil
	self.target.target.y = (type(target) == "table" and target.y) or nil
end