Skip to content
Snippets Groups Projects
Forked from tome / Tales of MajEyal
10827 commits behind the upstream repository.
Party.lua 11.38 KiB
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009, 2010, 2011 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.Entity"
local Map = require "engine.Map"
local Dialog = require "engine.ui.Dialog"
local GetQuantity = require "engine.dialogs.GetQuantity"
local PartyOrder = require "mod.dialogs.PartyOrder"

module(..., package.seeall, class.inherit(
	engine.Entity
))

function _M:init(t, no_default)
	engine.Entity.init(self, t, no_default)

	self.members = {}
	self.m_list = {}
	self.energy = {value = 0, mod=100000} -- "Act" every tick
end

function _M:addMember(actor, def)
	if self.members[actor] then
		print("[PARTY] error trying to add existing actor: ", actor.uid, actor.name)
		return false
	end
	if type(def.control) == "nil" then def.control = "no" end
	def.title = def.title or "Party member"
	self.members[actor] = def
	self.m_list[#self.m_list+1] = actor
	def.index = #self.m_list

	actor.ai_state = actor.ai_state or {}
	actor.ai_state.tactic_leash_anchor = actor.ai_state.tactic_leash_anchor or game.player
	actor.ai_state.tactic_leash = actor.ai_state.tactic_leash or 10

	actor.addEntityOrder = function(self, level)
		print("[PARTY] New member, add after", self.name, game.party.m_list[1].name)
		return game.party.m_list[1] -- Make the sure party is always consecutive in the level entities list
	end

	-- Turn NPCs into party members
	if not actor.no_party_class then
		actor:replaceWith(require("mod.class.PartyMember").new(actor))
	end

	-- Notify the UI
	if game.player then game.player.changed = true end
end

function _M:removeMember(actor, silent)
	if not self.members[actor] then
		if not silent then
			print("[PARTY] error trying to remove non-existing actor: ", actor.uid, actor.name)
		end
		return false
	end
	table.remove(self.m_list, self.members[actor].index)
	self.members[actor] = nil

	actor.addEntityOrder = nil

	-- Update indexes
	for i = 1, #self.m_list do
		self.members[self.m_list[i]].index = i
	end

	-- Notify the UI
	game.player.changed = true
end

function _M:leftLevel()
	local todel = {}
	local newplayer = false
	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]
		if def.temporary_level then
			todel[#todel+1] = actor
			if actor == game.player then newplayer = true end
		end
	end
	for i = 1, #todel do
		self:removeMember(todel[i])
		todel[i]:disappear()
	end
	self:findSuitablePlayer()
end

function _M:hasMember(actor)
	return self.members[actor]
end

function _M:findMember(filter)
	for i, actor in ipairs(self.m_list) do
		local ok = true
		local def = self.members[actor]

		if filter.main and not def.main then ok = false end
		if filter.type and def.type ~= filter.type then ok = false end

		if ok then return actor end
	end
end

function _M:setDeathTurn(actor, turn)
	local def = self.members[actor]
	if not def then return end
	def.last_death_turn = turn
end

function _M:findLastDeath()
	local max_turn = -9999
	local last = nil

	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]

		if def.last_death_turn and def.last_death_turn > max_turn then max_turn = def.last_death_turn; last = actor end
	end
	return last or self:findMember{main=true}
end

function _M:canControl(actor, vocal)
	if not actor then return false end
	if actor == game.player then return false end

	if game.player and game.player.no_leave_control then
--		print("[PARTY] error trying to set player but current player is modal")
		return false
	end
	if not self.members[actor] then
--		print("[PARTY] error trying to set player, not a member of party: ", actor.uid, actor.name)
		return false
	end
	if self.members[actor].control ~= "full" then
--		print("[PARTY] error trying to set player, not controlable: ", actor.uid, actor.name)
		return false
	end
	if actor.dead or (game.level and not game.level:hasEntity(actor)) then
		if vocal then game.logPlayer(game.player, "Can not switch control to this creature.") end
		return false
	end
	if actor.on_can_control and not actor:on_can_control(vocal) then return false end
	return true
end

function _M:setPlayer(actor, bypass)
	if type(actor) == "number" then actor = self.m_list[actor] end

	if not bypass then
		local ok, err = self:canControl(actor, true)
		if not ok then return nil, err end
	end

	if actor == game.player then return true end

	local def = self.members[actor]
	local oldp = self.player
	self.player = actor

	-- Convert the class to always be a player
	if actor.__CLASSNAME ~= "mod.class.Player" and not actor.no_party_class then
		actor.__PREVIOUS_CLASSNAME = actor.__CLASSNAME
		actor:replaceWith(mod.class.Player.new(actor))
		actor.changed = true
	end

	-- Setup as the curent player
	actor.player = true
	game.paused = actor:enoughEnergy()
	game.player = actor
	game.hotkeys_display.actor = actor
	Map:setViewerActor(actor)
	if game.target then game.target.source_actor = actor end
	if game.level then game.level.map:moveViewSurround(actor.x, actor.y, 8, 8) end
	actor._move_others = actor.move_others
	actor.move_others = true

	-- Change back the old actor to a normal actor
	if oldp and oldp ~= actor then
		if self.members[oldp] and self.members[oldp].on_uncontrol then self.members[oldp].on_uncontrol(oldp) end

		if oldp.__PREVIOUS_CLASSNAME then
			oldp:replaceWith(require(oldp.__PREVIOUS_CLASSNAME).new(oldp))
		end

		actor.move_others = actor._move_others
		oldp.changed = true
		oldp.player = nil
		if game.level and oldp.x and oldp.y then oldp:move(oldp.x, oldp.y, true) end
	end

	if def.on_control then def.on_control(actor) end

	if game.level and actor.x and actor.y then actor:move(actor.x, actor.y, true) end

	game.logPlayer(actor, "#MOCCASIN#Character control switched to %s.", actor.name)

	return true
end

function _M:findSuitablePlayer(type)
	for i, actor in ipairs(self.m_list) do
		local def = self.members[actor]
		if def.control == "full" and (not type or def.type == type) and not actor.dead and game.level:hasEntity(actor) then
			if self:setPlayer(actor, true) then
				return true
			end
		end
	end
	return false
end

function _M:canOrder(actor, order, vocal)
	if not actor then return false end
	if actor == game.player then return false end

	if not self.members[actor] then
		print("[PARTY] error trying to order, not a member of party: ", actor.uid, actor.name)
		return false
	end
	if (self.members[actor].control ~= "full" and self.members[actor].control ~= "order") or not self.members[actor].orders then
		print("[PARTY] error trying to order, not controlable: ", actor.uid, actor.name)
		return false
	end
	if actor.dead or (game.level and not game.level:hasEntity(actor)) then
		if vocal then game.logPlayer(game.player, "Can not give orders to this creature.") end
		return false
	end
	if actor.on_can_order and not actor:on_can_order(vocal) then return false end
	if order and not self.members[actor].orders[order] then
		print("[PARTY] error trying to order, unknown order: ", actor.uid, actor.name)
		return false
	end
	return true
end

function _M:giveOrders(actor)
	if type(actor) == "number" then actor = self.m_list[actor] end

	local ok, err = self:canOrder(actor, nil, true)
	if not ok then return nil, err end

	local def = self.members[actor]

	game:registerDialog(PartyOrder.new(actor, def))

	return true
end

function _M:giveOrder(actor, order)
	if type(actor) == "number" then actor = self.m_list[actor] end

	local ok, err = self:canOrder(actor, order, true)
	if not ok then return nil, err end

	local def = self.members[actor]

	if order == "leash" then
		game:registerDialog(GetQuantity.new("Set action radius: "..actor.name, "Set the maximun distance this creature can go from the party master", actor.ai_state.tactic_leash, actor.ai_state.tactic_leash_max or 100, function(qty)
			actor.ai_state.tactic_leash = util.bound(qty, 1, actor.ai_state.tactic_leash_max or 100)
			game.logPlayer(game.player, "%s maximum action radius set to %d.", actor.name:capitalize(), actor.ai_state.tactic_leash)
		end), 1)
	elseif order == "anchor" then
		local co = coroutine.create(function()
			local x, y, act = game.player:getTarget({type="hit", range=10})
			local anchor
			if x and y then
				if act then
					anchor = act
				else
					anchor = {x=x, y=y, name="that location"}
				end
				actor.ai_state.tactic_leash_anchor = anchor
				game.logPlayer(game.player, "%s will stay near %s.", actor.name:capitalize(), anchor.name)
			end
		end)
		local ok, err = coroutine.resume(co)
		if not ok and err then print(debug.traceback(co)) error(err) end
	elseif order == "target" then
		local co = coroutine.create(function()
			local x, y, act = game.player:getTarget({type="hit", range=10})
			if act then
				actor:setTarget(act)
				game.logPlayer(game.player, "%s targets %s.", actor.name:capitalize(), act.name)
			end
		end)
		local ok, err = coroutine.resume(co)
		if not ok and err then print(debug.traceback(co)) error(err) end
	elseif order == "behavior" then
		game:registerDialog(require("mod.dialogs.orders."..order:capitalize()).new(actor, def))
	elseif order == "talents" then
		game:registerDialog(require("mod.dialogs.orders."..order:capitalize()).new(actor, def))

	-------------------------------------------
	-- Escort specifics
	-------------------------------------------
	elseif order == "escort_rest" then
		-- Rest for a few turns
		if actor.ai_state.tactic_escort_rest then actor:doEmote("No, we must hurry!", 40) return true end
		actor.ai_state.tactic_escort_rest = rng.range(6, 10)
		actor:doEmote("Ok, but not for long.", 40)
	elseif order == "escort_portal" then
		local dist = core.fov.distance(actor.escort_target.x, actor.escort_target.y, actor.x, actor.y)
		if dist < 8 then dist = "very close"
		elseif dist < 16 then dist = "close"
		else dist = "still far away"
		end

		local dir
		if     actor.escort_target.x < actor.x and actor.escort_target.y < actor.y then dir = "north-west"
		elseif actor.escort_target.x > actor.x and actor.escort_target.y < actor.y then dir = "north-east"
		elseif actor.escort_target.x < actor.x and actor.escort_target.y > actor.y then dir = "south-west"
		elseif actor.escort_target.x > actor.x and actor.escort_target.y > actor.y then dir = "south-east"
		elseif actor.escort_target.x > actor.x and actor.escort_target.y == actor.y then dir = "east"
		elseif actor.escort_target.x < actor.x and actor.escort_target.y == actor.y then dir = "west"
		elseif actor.escort_target.x == actor.x and actor.escort_target.y < actor.y then dir = "north"
		elseif actor.escort_target.x == actor.x and actor.escort_target.y > actor.y then dir = "south"
		end

		actor:doEmote(("The portal is %s, to the %s."):format(dist, dir), 45)
	end

	return true
end

function _M:select(actor)
	if not actor then return false end
	if type(actor) == "number" then actor = self.m_list[actor] end
	if actor == game.player then return false end

	if self:canControl(actor) then return self:setPlayer(actor)
	elseif self:canOrder(actor) then return self:giveOrders(actor)
	end
	return false
end